feat: PTY kubectl终端 + 侧边栏对齐修复 + 构建缓存清理
**kubectl WebShell 修复:** - 引入 github.com/creack/pty 库实现真正的伪终端 - 替换 bash --noediting + TERM=dumb 方案为 PTY-backed bash - 支持 xterm-256color、窗口自适应、readline 交互 - 修复键盘输入无响应问题 **侧边栏优化:** - 独立菜单项高度从 py-1.5 改为 h-8,与父菜单对齐 - 独立菜单图标从 h-3 w-3 改为 h-3.5 w-3.5,统一 14px **构建优化:** - run.sh docker up 增加 docker image prune -f 清理悬空镜像 - 每次部署自动清理 builder cache + dangling images **认证修复(历史):** - Token 存储:Zustand + localStorage 双写,http.ts 双源 fallback - YamlViewer/DeploymentDetail token 来源统一为 Zustand store
This commit is contained in:
1 parent
cac1414917
commit
a3cd0d3910
29 files changed
+2954
-180
No files matched your search
@@ -0,0 +1,229 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
RefreshCw, Loader2, Cpu, Server, Container,
|
||||
ChevronRight, Cloud, Boxes, Network, FileText, Shield, Activity, Clock,
|
||||
} from 'lucide-react';
|
||||
import { kubernetesApi } from '@/services/api';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
const NAMESPACE_ICON: Record<string, React.ComponentType<any>> = {
|
||||
'kube-system': Server,
|
||||
'kube-public': Shield,
|
||||
'kube-node-lease': Activity,
|
||||
'default': Cloud,
|
||||
};
|
||||
|
||||
export default function ClusterOverviewPage() {
|
||||
const { clusterCode } = useParams<{ clusterCode: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [clusters, setClusters] = useState<any[]>([]);
|
||||
const [nodes, setNodes] = useState<any[]>([]);
|
||||
const [namespaces, setNamespaces] = useState<string[]>([]);
|
||||
const [clusterInfo, setClusterInfo] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Load cluster list + current cluster info
|
||||
const loadClusterInfo = useCallback(async () => {
|
||||
try {
|
||||
const res: any = await kubernetesApi.getClusters();
|
||||
const list = res?.list || [];
|
||||
setClusters(list);
|
||||
const current = list.find((c: any) => (c.code || String(c.id)) === clusterCode);
|
||||
setClusterInfo(current || null);
|
||||
if (!current && list.length > 0 && clusterCode) {
|
||||
toast({ title: '集群未找到', description: `找不到集群 ${clusterCode}`, variant: 'destructive' });
|
||||
}
|
||||
} catch { setClusters([]); }
|
||||
}, [clusterCode]);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
if (!clusterCode) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const [nodesRes, nsRes] = await Promise.all([
|
||||
kubernetesApi.getNodes(clusterCode).catch(() => []),
|
||||
kubernetesApi.getNamespaces(clusterCode).catch(() => []),
|
||||
]);
|
||||
setNodes(Array.isArray(nodesRes) ? nodesRes : (nodesRes?.list || nodesRes?.data || []));
|
||||
const nsList: string[] = Array.isArray(nsRes) ? nsRes : (nsRes?.list || nsRes?.data || []);
|
||||
setNamespaces(nsList);
|
||||
} catch { setNodes([]); setNamespaces([]); } finally { setLoading(false); }
|
||||
}, [clusterCode]);
|
||||
|
||||
useEffect(() => { loadClusterInfo(); }, [loadClusterInfo]);
|
||||
useEffect(() => { loadData(); }, [loadData]);
|
||||
|
||||
const getNodeRole = (node: any) => {
|
||||
if (node?.role === 'master' || node?.role === 'control-plane') return 'bg-blue-500/20 text-blue-400 border-blue-500/30';
|
||||
return 'bg-slate-500/20 text-slate-400 border-slate-500/30';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Breadcrumb */}
|
||||
<div className="flex items-center gap-2 text-sm text-slate-500">
|
||||
<Link to="/kubernetes" className="hover:text-slate-300 transition-colors">集群</Link>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
<span className="text-slate-300">{clusterCode}</span>
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-100">{clusterCode}</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
集群概览
|
||||
{clusterInfo?.version ? ` — Kubernetes ${clusterInfo.version}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="icon" className="h-9 w-9 border-slate-700 text-slate-400"
|
||||
onClick={loadData} disabled={loading}>
|
||||
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats cards */}
|
||||
<div className="grid grid-cols-3 md:grid-cols-3 gap-4">
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div><p className="text-sm text-slate-500">节点</p><p className="text-2xl font-bold text-blue-400">{nodes.length}</p></div>
|
||||
<Cpu className="h-8 w-8 opacity-30 text-blue-400" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div><p className="text-sm text-slate-500">命名空间</p><p className="text-2xl font-bold text-emerald-400">{namespaces.length}</p></div>
|
||||
<Boxes className="h-8 w-8 opacity-30 text-emerald-400" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div><p className="text-sm text-slate-500">版本</p><p className="text-lg font-bold text-slate-300">{clusterInfo?.version || '-'}</p></div>
|
||||
<Server className="h-8 w-8 opacity-30 text-slate-400" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Nodes table */}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-slate-100 mb-3 flex items-center gap-2">
|
||||
<Cpu className="h-4 w-4 text-blue-400" />节点列表
|
||||
</h2>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-slate-800 hover:bg-transparent">
|
||||
<TableHead className="text-slate-400">名称</TableHead>
|
||||
<TableHead className="text-slate-400">角色</TableHead>
|
||||
<TableHead className="text-slate-400">状态</TableHead>
|
||||
<TableHead className="text-slate-400">IP</TableHead>
|
||||
<TableHead className="text-slate-400">OS</TableHead>
|
||||
<TableHead className="text-slate-400">Kubelet</TableHead>
|
||||
<TableHead className="text-slate-400">CPU</TableHead>
|
||||
<TableHead className="text-slate-400">内存</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow><TableCell colSpan={8} className="text-center text-slate-500 py-12"><Loader2 className="h-5 w-5 animate-spin mx-auto mb-2" />加载中...</TableCell></TableRow>
|
||||
) : nodes.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={8} className="text-center text-slate-500 py-12"><Cpu className="h-8 w-8 mx-auto mb-2 opacity-50" />暂无节点</TableCell></TableRow>
|
||||
) : (
|
||||
nodes.map((n: any) => {
|
||||
const cpuCores = n.cpu || 0;
|
||||
const cpuUsageM = n.cpu_usage_m ? parseFloat(n.cpu_usage_m.replace('m','')) : 0;
|
||||
const cpuPct = cpuCores > 0 ? (cpuUsageM / 1000 / cpuCores) * 100 : 0;
|
||||
const memMb = n.memory_mb || 0;
|
||||
const memUsageMi = n.mem_usage_mi ? parseFloat(n.mem_usage_mi.replace('Mi','').replace('Ki','').replace('Gi','')) : 0;
|
||||
// Normalize to MiB
|
||||
const memUsageNorm = n.mem_usage_mi?.includes('Ki') ? memUsageMi / 1024 : n.mem_usage_mi?.includes('Gi') ? memUsageMi * 1024 : memUsageMi;
|
||||
const memPct = memMb > 0 ? (memUsageNorm / memMb) * 100 : 0;
|
||||
const color = (pct: number) => pct > 80 ? 'bg-red-500' : pct > 60 ? 'bg-yellow-500' : 'bg-emerald-500';
|
||||
return (
|
||||
<TableRow key={n.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||
<TableCell className="font-medium text-slate-200">{n.name}</TableCell>
|
||||
<TableCell><Badge className={getNodeRole(n)}>{n.role || 'worker'}</Badge></TableCell>
|
||||
<TableCell><Badge className={n.status === 'Ready' ? 'bg-emerald-500/20 text-emerald-400' : 'bg-red-500/20 text-red-400'}>{n.status || 'Unknown'}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{n.ip || '-'}</TableCell>
|
||||
<TableCell className="text-sm text-slate-400 max-w-[200px] truncate">{n.os || '-'}</TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{n.kubelet_version || '-'}</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{n.cpu_usage_m ? (
|
||||
<div className="flex items-center gap-1.5 min-w-[80px]">
|
||||
<div className="flex-1 h-1.5 rounded-full bg-slate-700/50 overflow-hidden">
|
||||
<div className={`h-full rounded-full ${color(cpuPct)}`} style={{ width: `${Math.min(cpuPct, 100)}%` }} />
|
||||
</div>
|
||||
<span className="text-slate-400">{cpuPct.toFixed(0)}%</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-slate-500">{cpuCores} 核</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{n.mem_usage_mi ? (
|
||||
<div className="flex items-center gap-1.5 min-w-[80px]">
|
||||
<div className="flex-1 h-1.5 rounded-full bg-slate-700/50 overflow-hidden">
|
||||
<div className={`h-full rounded-full ${color(memPct)}`} style={{ width: `${Math.min(memPct, 100)}%` }} />
|
||||
</div>
|
||||
<span className="text-slate-400">{memPct.toFixed(0)}%</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-slate-500">{memMb}Mi</span>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Namespace list */}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-slate-100 mb-3 flex items-center gap-2">
|
||||
<Boxes className="h-4 w-4 text-emerald-400" />命名空间
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{namespaces.map((ns) => {
|
||||
const Icon = NAMESPACE_ICON[ns] || Container;
|
||||
return (
|
||||
<Link key={ns}
|
||||
to={`/kubernetes/${clusterCode}/namespace/${ns}`}
|
||||
className="bg-slate-900 border border-slate-800 rounded-lg p-4 hover:border-slate-600 hover:bg-slate-800/50 transition-all group"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="h-5 w-5 text-slate-500 group-hover:text-slate-300 transition-colors" />
|
||||
<span className="text-sm font-medium text-slate-300 group-hover:text-slate-100 transition-colors">{ns}</span>
|
||||
</div>
|
||||
<ChevronRight className="h-4 w-4 text-slate-600 group-hover:text-slate-400 transition-colors" />
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user