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

@@ -0,0 +1,161 @@
// DnsHostSection.tsx — DNS 配置 + 主机别名 // Logan.Li
import { Globe, MapPin, Plus, Trash2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
export interface DnsOptionEntry {
name: string;
value: string;
}
export interface HostAliasEntry {
ip: string;
hostnames: string;
}
export interface DnsHostSectionProps {
dnsNameservers: string;
onDnsNameserversChange: (value: string) => void;
dnsSearches: string;
onDnsSearchesChange: (value: string) => void;
dnsOptions: DnsOptionEntry[];
onDnsOptionsChange: (options: DnsOptionEntry[]) => void;
hostAliases: HostAliasEntry[];
onHostAliasesChange: (aliases: HostAliasEntry[]) => void;
}
export function DnsHostSection({
dnsNameservers,
onDnsNameserversChange,
dnsSearches,
onDnsSearchesChange,
dnsOptions,
onDnsOptionsChange,
hostAliases,
onHostAliasesChange,
}: DnsHostSectionProps) {
const addDnsOption = () => onDnsOptionsChange([...dnsOptions, { name: '', value: '' }]);
const updateDnsOption = (index: number, patch: Partial<DnsOptionEntry>) =>
onDnsOptionsChange(dnsOptions.map((o, i) => (i === index ? { ...o, ...patch } : o)));
const removeDnsOption = (index: number) =>
onDnsOptionsChange(dnsOptions.filter((_, i) => i !== index));
const addHostAlias = () => onHostAliasesChange([...hostAliases, { ip: '', hostnames: '' }]);
const updateHostAlias = (index: number, patch: Partial<HostAliasEntry>) =>
onHostAliasesChange(hostAliases.map((a, i) => (i === index ? { ...a, ...patch } : a)));
const removeHostAlias = (index: number) =>
onHostAliasesChange(hostAliases.filter((_, i) => i !== index));
return (
<>
<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 => onDnsNameserversChange(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 => onDnsSearchesChange(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={addDnsOption}>
<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 => updateDnsOption(i, { name: e.target.value })}
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 => updateDnsOption(i, { value: e.target.value })}
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={() => removeDnsOption(i)}>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))
)}
</div>
</CardContent>
</Card>
<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={addHostAlias}>
<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 => updateHostAlias(i, { ip: e.target.value })}
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 => updateHostAlias(i, { hostnames: e.target.value })}
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={() => removeHostAlias(i)}>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))
)}
</CardContent>
</Card>
</>
);
}
@@ -0,0 +1,212 @@
// SchedulingSection.tsx — 调度:节点名称 / 标签选择器 / 节点亲和性 // Logan.Li
import { Loader2, Plus, Server, Trash2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
export type ScheduleMode = 'nodeName' | 'nodeSelector' | 'nodeAffinity';
export interface LabelSelectorEntry {
key: string;
value: string;
}
export interface ClusterNodeOption {
name: string;
ip: string;
labels: Record<string, string>;
}
export interface SchedulingSectionProps {
scheduleMode: ScheduleMode;
onScheduleModeChange: (mode: ScheduleMode) => void;
nodeName: string;
onNodeNameChange: (name: string) => void;
availableNodes: ClusterNodeOption[];
nodesLoading: boolean;
labelSelectorKv: LabelSelectorEntry[];
onAddLabelSelector: () => void;
onRemoveLabelSelector: (index: number) => void;
onUpdateLabelSelector: (index: number, patch: Partial<LabelSelectorEntry>) => void;
nodeLabels: Record<string, string[]>;
}
const SCHEDULE_MODES: Array<{ key: ScheduleMode; label: string }> = [
{ key: 'nodeName', label: '节点名称' },
{ key: 'nodeSelector', label: '标签选择器' },
{ key: 'nodeAffinity', label: '节点亲和性' },
];
export function SchedulingSection({
scheduleMode,
onScheduleModeChange,
nodeName,
onNodeNameChange,
availableNodes,
nodesLoading,
labelSelectorKv,
onAddLabelSelector,
onRemoveLabelSelector,
onUpdateLabelSelector,
nodeLabels,
}: SchedulingSectionProps) {
return (
<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">
{SCHEDULE_MODES.map(m => (
<button
key={m.key}
onClick={() => onScheduleModeChange(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 => onNodeNameChange(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={onAddLabelSelector}>
<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 => onUpdateLabelSelector(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 => onUpdateLabelSelector(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={() => onRemoveLabelSelector(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={onAddLabelSelector}>
<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 => onUpdateLabelSelector(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 => onUpdateLabelSelector(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={() => onRemoveLabelSelector(i)}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))
)}
</div>
)}
</CardContent>
</Card>
);
}