fix: K8s 集群添加缺少 code 必填字段
- KubernetesClusterDialog: 新增 code 表单字段 - 名称变更自动生成 code(小写+连字符) - handleSubmit 增加 code 校验 + toast 提示 - payload 增加 code 字段(后端 binding:required)
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user