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
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user