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:
Logan.Li committed 2026-06-26 00:02:29 +08:00
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>
);
}
}