refactor(frontend): API 子模块化、大组件拆分与 TanStack Query hooks
P1: kubernetes/system API 拆为子目录模块,导出工具下沉到 lib/export-utils,
http 统一 buildApiUrl/getWsBase;HostMonitor、DeploymentEdit* 拆至子组件。
P2: 新增 hooks/api(queryKeys、useTerminalSessions、useHostMonitor),
TerminalSessionsTab 与 HostMonitor 接入 TanStack Query。
P3: 保留 services/api 路径,未迁移至 api/ 目录。
This commit is contained in:
1 parent
7fb2236b4d
commit
0b404b4611
226 files changed
+3040
-2596
No files matched your search
@@ -1,4 +1,4 @@
|
||||
// DeploymentEditAdvanced.tsx — 编辑器「高级设置」Tab (M8)
|
||||
// DeploymentEditAdvanced.tsx — 编辑器「高级设置」Tab (M8) // Logan.Li
|
||||
// 编辑内容:更新策略 / 历史版本 / 调度(节点选择/容忍) / DNS / 安全上下文 / 终止宽限期 / 调度器 / SA
|
||||
// 作者: Logan <admin@attacker.club>
|
||||
import { useState, useEffect } from 'react';
|
||||
@@ -7,10 +7,12 @@ import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Save, Loader2, Settings, RefreshCw, Server, Shield, Clock, Network, Plus, Trash2, Globe, MapPin,
|
||||
Save, Loader2, Settings, RefreshCw, Shield, Clock, Network,
|
||||
} from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { kubernetesApi } from '@/services/api';
|
||||
import { SchedulingSection, type ScheduleMode } from './deployment-edit-advanced/SchedulingSection';
|
||||
import { DnsHostSection } from './deployment-edit-advanced/DnsHostSection';
|
||||
|
||||
interface Props {
|
||||
clusterCode: string;
|
||||
@@ -45,13 +47,11 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
|
||||
const [progressDeadlineSeconds, setProgressDeadlineSeconds] = useState('600');
|
||||
|
||||
// ──── 调度策略 ────
|
||||
const [scheduleMode, setScheduleMode] = useState<'nodeName' | 'nodeSelector' | 'nodeAffinity'>('nodeSelector');
|
||||
const [scheduleMode, setScheduleMode] = useState<ScheduleMode>('nodeSelector');
|
||||
const [nodeName, setNodeName] = useState('');
|
||||
const [availableNodes, setAvailableNodes] = useState<Array<{ name: string; ip: string; labels: Record<string, string> }>>([]);
|
||||
const [nodesLoading, setNodesLoading] = useState(false);
|
||||
// 标签选择器 — key=value 列表(替代原 nodeSelectorKv)
|
||||
const [labelSelectorKv, setLabelSelectorKv] = useState<Array<{ key: string; value: string }>>([]);
|
||||
// 预置标签(从节点聚合)
|
||||
const [nodeLabels, setNodeLabels] = useState<Record<string, string[]>>({});
|
||||
const [tolerations, setTolerations] = useState<Toleration[]>([]);
|
||||
const [dnsPolicy, setDnsPolicy] = useState('ClusterFirst');
|
||||
@@ -118,13 +118,13 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
|
||||
// 加载节点列表
|
||||
if (clusterCode) {
|
||||
setNodesLoading(true);
|
||||
kubernetesApi.getClusterNodes(clusterCode).then(( res: any) => {
|
||||
kubernetesApi.getClusterNodes(clusterCode).then((res: any) => {
|
||||
const nodes: Record<string, any>[] = Array.isArray(res) ? res : (res?.items || res?.list || []);
|
||||
const mapped = nodes.map(( n: any) => {
|
||||
const mapped = nodes.map((n: any) => {
|
||||
const meta = n.metadata || {};
|
||||
const status = n.status || {};
|
||||
const addresses: Record<string, any>[] = Array.isArray(status.addresses) ? status.addresses : [];
|
||||
const internalIP = addresses.find(( a: any) => a.type === 'InternalIP')?.address || '';
|
||||
const internalIP = addresses.find((a: any) => a.type === 'InternalIP')?.address || '';
|
||||
return { name: meta.name || '', ip: internalIP, labels: meta.labels || {} };
|
||||
});
|
||||
setAvailableNodes(mapped);
|
||||
@@ -193,12 +193,6 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
|
||||
const updateNs = (i: number, patch: Partial<{ key: string; value: string }>) =>
|
||||
setLabelSelectorKv(prev => prev.map((n, idx) => (idx === i ? { ...n, ...patch } : n)));
|
||||
|
||||
// tolerations 操作
|
||||
const _addTol = () => setTolerations(prev => [...prev, { key: '', operator: 'Equal', value: '', effect: 'NoSchedule', tolerationSeconds: '' }]);
|
||||
const _removeTol = (i: number) => setTolerations(prev => prev.filter((_, idx) => idx !== i));
|
||||
const _updateTol = (i: number, patch: Partial<Toleration>) =>
|
||||
setTolerations(prev => prev.map((t, idx) => (idx === i ? { ...t, ...patch } : t)));
|
||||
|
||||
const save = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
@@ -404,137 +398,19 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 调度:节点名称 / 标签选择器 / 节点亲和性 */}
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-sm text-slate-200 flex items-center gap-2">
|
||||
<Server className="h-4 w-4 text-emerald-400" />调度 (Scheduling)
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-1 bg-slate-800 rounded-lg p-0.5 border border-slate-700">
|
||||
{([
|
||||
{ key: 'nodeName' as const, label: '节点名称', desc: '指定节点' },
|
||||
{ key: 'nodeSelector' as const, label: '标签选择器', desc: 'key=value' },
|
||||
{ key: 'nodeAffinity' as const, label: '节点亲和性', desc: '高级规则' },
|
||||
]).map(m => (
|
||||
<button key={m.key} onClick={() => setScheduleMode(m.key)}
|
||||
className={`px-2.5 py-1 rounded text-[10px] font-medium transition-all ${
|
||||
scheduleMode === m.key
|
||||
? 'bg-emerald-500/20 text-emerald-300 shadow-sm'
|
||||
: 'text-slate-400 hover:text-slate-300'
|
||||
}`}>
|
||||
{m.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
{scheduleMode === 'nodeName' && 'Pod 将直接调度到指定节点'}
|
||||
{scheduleMode === 'nodeSelector' && 'Pod 将调度到匹配标签的节点'}
|
||||
{scheduleMode === 'nodeAffinity' && '通过亲和性规则控制 Pod 调度到指定节点'}
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
{/* 节点名称模式 */}
|
||||
{scheduleMode === 'nodeName' && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-slate-400 text-xs">请选择节点</Label>
|
||||
{nodesLoading ? (
|
||||
<div className="flex items-center gap-1 text-slate-500 text-xs"><Loader2 className="h-3 w-3 animate-spin" />加载节点列表...</div>
|
||||
) : (
|
||||
<select value={nodeName} onChange={e => setNodeName(e.target.value)}
|
||||
className="h-8 w-full bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-2">
|
||||
<option value="">(不指定)</option>
|
||||
{availableNodes.map(n => (
|
||||
<option key={n.name} value={n.name}>{n.name} {n.ip ? `(${n.ip})` : ''}</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
{nodeName && (
|
||||
<div className="text-[10px] text-slate-500">
|
||||
已选择: <code className="bg-slate-800 px-1 py-0.5 rounded text-emerald-400">{nodeName}</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 标签选择器模式 */}
|
||||
{scheduleMode === 'nodeSelector' && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-slate-400 text-xs">请选择标签</Label>
|
||||
<Button size="sm" variant="ghost" className="h-6 text-xs text-slate-400" onClick={addNs}>
|
||||
<Plus className="h-3 w-3 mr-1" />添加
|
||||
</Button>
|
||||
</div>
|
||||
{labelSelectorKv.length === 0 ? (
|
||||
<p className="text-xs text-slate-600 italic">未配置标签选择器</p>
|
||||
) : labelSelectorKv.map((n, i) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
{/* 标签 Key 下拉 */}
|
||||
<select value={n.key} onChange={e => updateNs(i, { key: e.target.value, value: '' })}
|
||||
className="flex-1 h-7 bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-1.5">
|
||||
<option value="">选择标签</option>
|
||||
{Object.entries(nodeLabels).map(([k, vals]) => (
|
||||
<option key={k} value={k}>{k} ({vals.length})</option>
|
||||
))}
|
||||
</select>
|
||||
{/* 标签 Value 下拉 */}
|
||||
<select value={n.value} onChange={e => updateNs(i, { value: e.target.value })}
|
||||
className="flex-1 h-7 bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-1.5"
|
||||
disabled={!n.key}>
|
||||
<option value="">选择值</option>
|
||||
{(nodeLabels[n.key] || []).map(v => (
|
||||
<option key={v} value={v}>{v}</option>
|
||||
))}
|
||||
</select>
|
||||
<Button size="icon" variant="ghost" className="h-6 w-6 text-red-400" onClick={() => removeNs(i)}>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 节点亲和性模式 */}
|
||||
{scheduleMode === 'nodeAffinity' && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-slate-400 text-xs">
|
||||
请选择标签 <span className="text-slate-600">(生成 requiredDuringScheduling 规则)</span>
|
||||
</Label>
|
||||
<Button size="sm" variant="ghost" className="h-6 text-xs text-slate-400" onClick={addNs}>
|
||||
<Plus className="h-3 w-3 mr-1" />添加
|
||||
</Button>
|
||||
</div>
|
||||
{labelSelectorKv.length === 0 ? (
|
||||
<p className="text-xs text-slate-600 italic">未配置亲和性规则</p>
|
||||
) : labelSelectorKv.map((n, i) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<select value={n.key} onChange={e => updateNs(i, { key: e.target.value, value: '' })}
|
||||
className="flex-1 h-7 bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-1.5">
|
||||
<option value="">选择标签</option>
|
||||
{Object.entries(nodeLabels).map(([k, vals]) => (
|
||||
<option key={k} value={k}>{k} ({vals.length})</option>
|
||||
))}
|
||||
</select>
|
||||
<select value={n.value} onChange={e => updateNs(i, { value: e.target.value })}
|
||||
className="flex-1 h-7 bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-1.5"
|
||||
disabled={!n.key}>
|
||||
<option value="">选择值</option>
|
||||
{(nodeLabels[n.key] || []).map(v => (
|
||||
<option key={v} value={v}>{v}</option>
|
||||
))}
|
||||
</select>
|
||||
<Button size="icon" variant="ghost" className="h-6 w-6 text-red-400" onClick={() => removeNs(i)}>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<SchedulingSection
|
||||
scheduleMode={scheduleMode}
|
||||
onScheduleModeChange={setScheduleMode}
|
||||
nodeName={nodeName}
|
||||
onNodeNameChange={setNodeName}
|
||||
availableNodes={availableNodes}
|
||||
nodesLoading={nodesLoading}
|
||||
labelSelectorKv={labelSelectorKv}
|
||||
onAddLabelSelector={addNs}
|
||||
onRemoveLabelSelector={removeNs}
|
||||
onUpdateLabelSelector={updateNs}
|
||||
nodeLabels={nodeLabels}
|
||||
/>
|
||||
|
||||
{/* Pod 级别杂项 */}
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
@@ -645,92 +521,16 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* DNS 配置(M8 补全) */}
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-slate-200 flex items-center gap-2">
|
||||
<Globe className="h-4 w-4 text-indigo-400" />DNS 配置 (dnsConfig)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 space-y-3">
|
||||
<p className="text-[11px] text-slate-500">
|
||||
提示:dnsConfig 通常与 <code className="text-slate-400">dnsPolicy: None</code> 搭配使用。每行一个值。
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-slate-500 text-xs">nameservers(每行一个 IP)</Label>
|
||||
<textarea value={dnsNameservers} onChange={e => setDnsNameservers(e.target.value)}
|
||||
placeholder={'8.8.8.8\n114.114.114.114'}
|
||||
rows={3}
|
||||
className="w-full mt-1 bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 font-mono p-2 resize-y" />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-500 text-xs">searches(搜索域,每行一个)</Label>
|
||||
<textarea value={dnsSearches} onChange={e => setDnsSearches(e.target.value)}
|
||||
placeholder={'svc.cluster.local\ncluster.local'}
|
||||
rows={3}
|
||||
className="w-full mt-1 bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 font-mono p-2 resize-y" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<Label className="text-slate-400 text-xs">options(DNS 选项)</Label>
|
||||
<Button size="sm" variant="ghost" className="h-6 text-xs text-slate-400"
|
||||
onClick={() => setDnsOptions(prev => [...prev, { name: '', value: '' }])}>
|
||||
<Plus className="h-3 w-3 mr-1" />添加
|
||||
</Button>
|
||||
</div>
|
||||
{dnsOptions.length === 0 ? (
|
||||
<p className="text-xs text-slate-600 italic">未配置</p>
|
||||
) : dnsOptions.map((o, i) => (
|
||||
<div key={i} className="flex items-center gap-2 mb-1.5">
|
||||
<Input value={o.name} onChange={e => setDnsOptions(prev => prev.map((x, idx) => idx === i ? { ...x, name: e.target.value } : x))}
|
||||
placeholder="ndots" className="flex-1 h-7 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
|
||||
<span className="text-slate-600">:</span>
|
||||
<Input value={o.value} onChange={e => setDnsOptions(prev => prev.map((x, idx) => idx === i ? { ...x, value: e.target.value } : x))}
|
||||
placeholder="5(可留空)" className="flex-1 h-7 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
|
||||
<Button size="icon" variant="ghost" className="h-6 w-6 text-red-400"
|
||||
onClick={() => setDnsOptions(prev => prev.filter((_, idx) => idx !== i))}>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 主机别名(M8 补全) */}
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-slate-200 flex items-center gap-2">
|
||||
<MapPin className="h-4 w-4 text-amber-400" />主机别名 (hostAliases)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 space-y-2">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<Label className="text-slate-400 text-xs">IP → 主机名映射(注入 Pod 的 /etc/hosts)</Label>
|
||||
<Button size="sm" variant="ghost" className="h-6 text-xs text-slate-400"
|
||||
onClick={() => setHostAliases(prev => [...prev, { ip: '', hostnames: '' }])}>
|
||||
<Plus className="h-3 w-3 mr-1" />添加
|
||||
</Button>
|
||||
</div>
|
||||
{hostAliases.length === 0 ? (
|
||||
<p className="text-xs text-slate-600 italic">未配置</p>
|
||||
) : hostAliases.map((a, i) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<Input value={a.ip} onChange={e => setHostAliases(prev => prev.map((x, idx) => idx === i ? { ...x, ip: e.target.value } : x))}
|
||||
placeholder="10.10.10.72" className="w-40 h-7 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
|
||||
<span className="text-slate-600">→</span>
|
||||
<Input value={a.hostnames} onChange={e => setHostAliases(prev => prev.map((x, idx) => idx === i ? { ...x, hostnames: e.target.value } : x))}
|
||||
placeholder="gitlab.local, registry.local" className="flex-1 h-7 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
|
||||
<Button size="icon" variant="ghost" className="h-6 w-6 text-red-400"
|
||||
onClick={() => setHostAliases(prev => prev.filter((_, idx) => idx !== i))}>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<DnsHostSection
|
||||
dnsNameservers={dnsNameservers}
|
||||
onDnsNameserversChange={setDnsNameservers}
|
||||
dnsSearches={dnsSearches}
|
||||
onDnsSearchesChange={setDnsSearches}
|
||||
dnsOptions={dnsOptions}
|
||||
onDnsOptionsChange={setDnsOptions}
|
||||
hostAliases={hostAliases}
|
||||
onHostAliasesChange={setHostAliases}
|
||||
/>
|
||||
|
||||
{/* 操作栏 */}
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
@@ -742,4 +542,4 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user