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:
attacker committed 2026-06-19 11:22:45 +00:00
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>
);
}