fix: K8s 集群添加缺少 code 必填字段

- KubernetesClusterDialog: 新增 code 表单字段
- 名称变更自动生成 code(小写+连字符)
- handleSubmit 增加 code 校验 + toast 提示
- payload 增加 code 字段(后端 binding:required)
This commit is contained in:
attacker committed 2026-06-21 09:50:07 +00:00
1 parent f24fe43ac0
commit ce438c9f05
18 files changed
+3941 -144

No files matched your search

@@ -9,14 +9,24 @@ import { Label } from '@/components/ui/label';
import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from '@/components/ui/table';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
ArrowLeft, Minus, Plus, RotateCw, Upload, FileText, Terminal,
Gauge, Clock, Play, Pause, Trash2, RefreshCw, Loader2,
CheckCircle, AlertTriangle, XCircle, ExternalLink,
CheckCircle, AlertTriangle, XCircle, ExternalLink, FolderOpen,
} from 'lucide-react';
import { kubernetesApi } from '@/services/api';
import { useToast } from '@/hooks/use-toast';
import { useUserStore } from '@/store';
import { PodExecTerminal } from './PodExecTerminal';
import { PodLogsViewer } from './PodLogsViewer';
import { PodFileBrowser } from './PodFileBrowser';
interface DeploymentDetailProps {
clusterCode: string;
@@ -44,6 +54,25 @@ export function DeploymentDetail({ clusterCode, namespace, deploymentName, onBac
const [showLogs, setShowLogs] = useState(false);
const [logTail, setLogTail] = useState(200);
// 🆕 Pod exec / logs / file browser (Logan <admin@attacker.club>)
const [execPod, setExecPod] = useState<{ name: string; containers: string[] } | null>(null);
const [execOpen, setExecOpen] = useState(false);
const [logsViewerPod, setLogsViewerPod] = useState<{ name: string; containers: string[] } | null>(null);
const [logsViewerOpen, setLogsViewerOpen] = useState(false);
const [fileBrowserPod, setFileBrowserPod] = useState<{ name: string; containers: string[] } | null>(null);
const [fileBrowserOpen, setFileBrowserOpen] = useState(false);
// Logs tab 内的 Pod 选择器(增强版查看器入口)
const [logsTabPod, setLogsTabPod] = useState<string>('');
// 从 Pod 对象提取容器名列表(兼容多种字段)
const extractContainers = (pod: any): string[] => {
const c = pod?.containers;
if (Array.isArray(c)) return c.map((x: any) => typeof x === 'string' ? x : (x?.name || x?.Name)).filter(Boolean);
if (Array.isArray(pod?.container_statuses)) return pod.container_statuses.map((x: any) => x?.name).filter(Boolean);
if (Array.isArray(pod?.containerNames)) return pod.containerNames.filter(Boolean);
return [];
};
const loadData = useCallback(async () => {
setLoading(true);
try {
@@ -359,13 +388,29 @@ export function DeploymentDetail({ clusterCode, namespace, deploymentName, onBac
<TableCell>
<div className="flex items-center gap-1">
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-blue-400"
title="查看日志" onClick={() => handleLoadLogs(pod.name)}>
title="查看日志"
onClick={() => {
setLogsViewerPod({ name: pod.name, containers: extractContainers(pod) });
setLogsViewerOpen(true);
}}>
<FileText className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-emerald-400"
title="终端" onClick={() => toast({ title: '终端功能开发中', description: '需要 SPDY/WebSocket 支持' })}>
title="exec 终端"
onClick={() => {
setExecPod({ name: pod.name, containers: extractContainers(pod) });
setExecOpen(true);
}}>
<Terminal className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-amber-400"
title="文件浏览器"
onClick={() => {
setFileBrowserPod({ name: pod.name, containers: extractContainers(pod) });
setFileBrowserOpen(true);
}}>
<FolderOpen className="h-3.5 w-3.5" />
</Button>
</div>
</TableCell>
</TableRow>
@@ -384,6 +429,19 @@ export function DeploymentDetail({ clusterCode, namespace, deploymentName, onBac
<div className="flex items-center justify-between">
<CardTitle className="text-sm text-slate-400">Pod 日志</CardTitle>
<div className="flex items-center gap-2">
{showLogs && logPod && (
<Button
variant="outline" size="sm"
className="border-blue-700 text-blue-400 text-xs h-7"
onClick={() => {
const pod = pods.find((p: any) => p.name === logPod);
setLogsViewerPod({ name: logPod, containers: pod ? extractContainers(pod) : [] });
setLogsViewerOpen(true);
}}
>
<ExternalLink className="h-3 w-3 mr-1" />增强版日志
</Button>
)}
<span className="text-xs text-slate-500">最近</span>
<Input
type="number" value={logTail} onChange={(e) => setLogTail(Number(e.target.value))}
@@ -394,41 +452,45 @@ export function DeploymentDetail({ clusterCode, namespace, deploymentName, onBac
</div>
</CardHeader>
<CardContent>
{!showLogs ? (
{pods.length === 0 ? (
<div className="text-center py-12 text-slate-500">
<FileText className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p>点击 Pod 列表中的「日志」按钮查看</p>
<p>暂无 Pod</p>
</div>
) : (
<div>
<div className="flex items-center gap-2 mb-2">
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300 text-xs h-7"
onClick={async () => {
setLogs('');
// Stream mode
const token = useUserStore.getState().token || '';
const url = `${window.location.protocol}//${window.location.host}/api/v1/kubernetes/${clusterCode}/namespace/${namespace}/pods/${logPod}/logs?tail=${logTail}&stream=true`;
try {
const resp = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
const reader = resp.body?.getReader();
if (!reader) return;
const decoder = new TextDecoder();
let streamLogs = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
streamLogs += decoder.decode(value, { stream: true });
setLogs(streamLogs);
}
} catch (e) {
setLogs('流式连接中断');
}
}}>
<RefreshCw className="h-3 w-3 mr-1" />实时流
</Button>
<span className="text-xs text-slate-500">{logPod}</span>
</div>
<pre className="bg-slate-950 rounded-lg p-4 text-xs text-slate-300 font-mono overflow-auto max-h-[500px] whitespace-pre-wrap">
<div className="flex items-center gap-3 py-10 justify-center flex-wrap">
<FileText className="h-5 w-5 text-blue-400" />
<span className="text-sm text-slate-400">选择 Pod 打开增强版日志查看器</span>
<Select value={logsTabPod} onValueChange={setLogsTabPod}>
<SelectTrigger className="w-[280px] h-8 bg-slate-800 border-slate-700 text-slate-200 text-xs">
<SelectValue placeholder="选择 Pod…" />
</SelectTrigger>
<SelectContent>
{pods.map((p: any) => (
<SelectItem key={p.name} value={p.name}>{p.name}</SelectItem>
))}
</SelectContent>
</Select>
<Button
size="sm"
className="bg-blue-600 hover:bg-blue-700 h-8"
disabled={!logsTabPod}
onClick={() => {
const pod = pods.find((p: any) => p.name === logsTabPod);
setLogsViewerPod({ name: logsTabPod, containers: pod ? extractContainers(pod) : [] });
setLogsViewerOpen(true);
}}
>
<ExternalLink className="h-3 w-3 mr-1" />打开日志
</Button>
<span className="text-xs text-slate-600 block w-full text-center mt-2">
支持实时跟踪、多容器切换、previous 日志、关键字过滤、导出
</span>
</div>
)}
{showLogs && logPod && (
<div className="mt-2">
<pre className="bg-slate-950 rounded-lg p-4 text-xs text-slate-300 font-mono overflow-auto max-h-[300px] whitespace-pre-wrap">
{logs || '无日志内容'}
</pre>
</div>
@@ -499,6 +561,52 @@ export function DeploymentDetail({ clusterCode, namespace, deploymentName, onBac
</Card>
</TabsContent>
</Tabs>
{/* 🆕 Pod exec 终端 */}
{execPod && (
<PodExecTerminal
open={execOpen}
onOpenChange={(o) => {
setExecOpen(o);
if (!o) setExecPod(null);
}}
clusterId={clusterCode}
clusterCode={clusterCode}
namespace={namespace}
podName={execPod.name}
containers={execPod.containers}
/>
)}
{/* 🆕 增强版日志查看器 */}
{logsViewerPod && (
<PodLogsViewer
open={logsViewerOpen}
onOpenChange={(o) => {
setLogsViewerOpen(o);
if (!o) setLogsViewerPod(null);
}}
clusterCode={clusterCode}
namespace={namespace}
podName={logsViewerPod.name}
containers={logsViewerPod.containers}
/>
)}
{/* 🆕 容器文件浏览器 */}
{fileBrowserPod && (
<PodFileBrowser
open={fileBrowserOpen}
onOpenChange={(o) => {
setFileBrowserOpen(o);
if (!o) setFileBrowserPod(null);
}}
clusterCode={clusterCode}
namespace={namespace}
podName={fileBrowserPod.name}
containers={fileBrowserPod.containers}
/>
)}
</div>
);
}