feat: Agent CLI 与终端审计回放全面优化
- Agent CLI: 多轮对话、SSH 长连接、流式推理、Evolver 按用户隔离、历史侧栏 - 终端审计: 回放播放器、命令书签、筛选分页导出、命令输出统计、录制落盘修复 - 修复 session_commands 表迁移、Kill 竞态导致录制丢失、无录制会话回放提示 - 审计历史页只读(移除删除),移除回放弹框录制搜索
This commit is contained in:
1 parent
9a3dd304f7
commit
1acb0c49af
81 files changed
+3916
-1655
No files matched your search
@@ -74,30 +74,30 @@ export default function ClusterOverviewPage() {
|
||||
useEffect(() => { loadData(); }, [loadData]);
|
||||
|
||||
const getNodeRole = (node: JsonData) => {
|
||||
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';
|
||||
if (node?.role === 'master' || node?.role === 'control-plane') return 'bg-[var(--dg-blue)]/20 text-[var(--dg-blue)] border-[var(--dg-blue)]/30';
|
||||
return 'bg-[var(--dg-text-muted)]/20 text-[var(--dg-text-secondary)] border-[var(--dg-text-muted)]/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>
|
||||
<div className="flex items-center gap-2 text-sm text-[var(--dg-text-muted)]">
|
||||
<Link to="/kubernetes" className="hover:text-[var(--dg-text-secondary)] transition-colors">集群</Link>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
<span className="text-slate-300">{clusterCode}</span>
|
||||
<span className="text-[var(--dg-text-secondary)]">{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">
|
||||
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)]">{clusterCode}</h1>
|
||||
<p className="text-sm text-[var(--dg-text-muted)] 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"
|
||||
<Button variant="outline" size="icon" className="h-9 w-9 border-[var(--dg-border)] text-[var(--dg-text-secondary)]"
|
||||
onClick={loadData} disabled={loading}>
|
||||
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
|
||||
</Button>
|
||||
@@ -106,27 +106,27 @@ export default function ClusterOverviewPage() {
|
||||
|
||||
{/* Stats cards */}
|
||||
<div className="grid grid-cols-3 md:grid-cols-3 gap-4">
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<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><p className="text-sm text-[var(--dg-text-muted)]">节点</p><p className="text-2xl font-bold text-[var(--dg-blue)]">{nodes.length}</p></div>
|
||||
<Cpu className="h-8 w-8 opacity-30 text-[var(--dg-blue)]" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<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><p className="text-sm text-[var(--dg-text-muted)]">命名空间</p><p className="text-2xl font-bold text-[var(--dg-green)]">{namespaces.length}</p></div>
|
||||
<Boxes className="h-8 w-8 opacity-30 text-[var(--dg-green)]" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<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><p className="text-sm text-[var(--dg-text-muted)]">版本</p><p className="text-lg font-bold text-[var(--dg-text-secondary)]">{clusterInfo?.version || '-'}</p></div>
|
||||
<Server className="h-8 w-8 opacity-30 text-[var(--dg-text-secondary)]" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -134,29 +134,29 @@ export default function ClusterOverviewPage() {
|
||||
|
||||
{/* 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 className="text-lg font-semibold text-[var(--dg-text-primary)] mb-3 flex items-center gap-2">
|
||||
<Cpu className="h-4 w-4 text-[var(--dg-blue)]" />节点列表
|
||||
</h2>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<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 className="border-[var(--dg-border)] hover:bg-transparent">
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">名称</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">角色</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">状态</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">IP</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">OS</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">Kubelet</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">CPU</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">内存</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>
|
||||
<TableRow><TableCell colSpan={8} className="text-center text-[var(--dg-text-muted)] 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>
|
||||
<TableRow><TableCell colSpan={8} className="text-center text-[var(--dg-text-muted)] py-12"><Cpu className="h-8 w-8 mx-auto mb-2 opacity-50" />暂无节点</TableCell></TableRow>
|
||||
) : (
|
||||
nodes.map((n: JsonData) => {
|
||||
const cpuCores = n.cpu || 0;
|
||||
@@ -167,38 +167,38 @@ export default function ClusterOverviewPage() {
|
||||
// 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';
|
||||
const color = (pct: number) => pct > 80 ? 'bg-[var(--dg-red)]' : pct > 60 ? 'bg-[var(--dg-orange)]' : 'bg-[var(--dg-green)]';
|
||||
return (
|
||||
<TableRow key={n.name} className="border-slate-800 hover:bg-slate-800/30 cursor-pointer"
|
||||
<TableRow key={n.name} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/30 cursor-pointer"
|
||||
onClick={() => navigate(`/kubernetes/${clusterCode}/nodes/${n.name}`)}>
|
||||
<TableCell className="font-medium text-blue-400 hover:text-blue-300">{n.name}</TableCell>
|
||||
<TableCell className="font-medium text-[var(--dg-blue)] hover:text-[var(--dg-blue)]">{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><Badge className={n.status === 'Ready' ? 'bg-[var(--dg-green)]/20 text-[var(--dg-green)]' : 'bg-[var(--dg-red)]/20 text-[var(--dg-red)]'}>{n.status || 'Unknown'}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{n.ip || '-'}</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)] max-w-[200px] truncate">{n.os || '-'}</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{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="flex-1 h-1.5 rounded-full bg-[var(--dg-bg-tertiary)]/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>
|
||||
<span className="text-[var(--dg-text-secondary)]">{cpuPct.toFixed(0)}%</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-slate-500">{cpuCores} 核</span>
|
||||
<span className="text-[var(--dg-text-muted)]">{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="flex-1 h-1.5 rounded-full bg-[var(--dg-bg-tertiary)]/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>
|
||||
<span className="text-[var(--dg-text-secondary)]">{memPct.toFixed(0)}%</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-slate-500">{memMb}Mi</span>
|
||||
<span className="text-[var(--dg-text-muted)]">{memMb}Mi</span>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -214,10 +214,10 @@ export default function ClusterOverviewPage() {
|
||||
{/* Namespace list */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
|
||||
<Boxes className="h-4 w-4 text-emerald-400" />命名空间
|
||||
<h2 className="text-lg font-semibold text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<Boxes className="h-4 w-4 text-[var(--dg-green)]" />命名空间
|
||||
</h2>
|
||||
<Link to={`/kubernetes/${clusterCode}/namespaces`} className="text-sm text-blue-400 hover:text-blue-300 transition-colors flex items-center gap-1">
|
||||
<Link to={`/kubernetes/${clusterCode}/namespaces`} className="text-sm text-[var(--dg-blue)] hover:text-[var(--dg-blue)] transition-colors flex items-center gap-1">
|
||||
管理全部 <ChevronRight className="h-3 w-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@@ -227,14 +227,14 @@ export default function ClusterOverviewPage() {
|
||||
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"
|
||||
className="bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-lg p-4 hover:border-[var(--dg-border-hover)] hover:bg-[var(--dg-bg-tertiary)]/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>
|
||||
<Icon className="h-5 w-5 text-[var(--dg-text-muted)] group-hover:text-[var(--dg-text-secondary)] transition-colors" />
|
||||
<span className="text-sm font-medium text-[var(--dg-text-secondary)] group-hover:text-[var(--dg-text-primary)] transition-colors">{ns}</span>
|
||||
</div>
|
||||
<ChevronRight className="h-4 w-4 text-slate-600 group-hover:text-slate-400 transition-colors" />
|
||||
<ChevronRight className="h-4 w-4 text-[var(--dg-text-muted)] group-hover:text-[var(--dg-text-secondary)] transition-colors" />
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -124,7 +124,7 @@ export function EventTable({
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={isWarning
|
||||
? 'text-[var(--dg-warning)] border-[var(--dg-warning)]/30 text-[10px]'
|
||||
? 'text-[var(--dg-orange)] border-[var(--dg-orange)]/30 text-[10px]'
|
||||
: 'text-[var(--dg-text-muted)] border-[var(--dg-border)] text-[10px]'}
|
||||
>
|
||||
{e.type}
|
||||
|
||||
@@ -48,7 +48,7 @@ function RankPanel({
|
||||
</span>
|
||||
<div className="flex-1 h-1.5 rounded-full bg-[var(--dg-bg-tertiary)] overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full ${active ? 'bg-[var(--dg-accent)]' : 'bg-[var(--dg-text-muted)]/50'}`}
|
||||
className={`h-full rounded-full ${active ? 'bg-[var(--dg-blue)]' : 'bg-[var(--dg-text-muted)]/50'}`}
|
||||
style={{ width: `${(item.count / max) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -254,17 +254,17 @@ export default function EventsPage() {
|
||||
)}
|
||||
|
||||
{summary?.recent_warnings?.length > 0 && (
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] border-l-2 border-l-[var(--dg-warning)]">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] border-l-2 border-l-[var(--dg-orange)]">
|
||||
<CardContent className="p-4">
|
||||
<h3 className="text-sm font-medium text-[var(--dg-text-primary)] mb-3 flex items-center gap-1.5">
|
||||
<AlertTriangle className="h-4 w-4 text-[var(--dg-warning)]" />
|
||||
<AlertTriangle className="h-4 w-4 text-[var(--dg-orange)]" />
|
||||
最新告警 ({summary.recent_warnings.length})
|
||||
</h3>
|
||||
<div className="space-y-1">
|
||||
{summary.recent_warnings.map((w: JsonData, i: number) => (
|
||||
<div key={i} className="text-xs py-1.5 px-2 rounded hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="shrink-0 font-mono text-[var(--dg-warning)]">{w.reason}</span>
|
||||
<span className="shrink-0 font-mono text-[var(--dg-orange)]">{w.reason}</span>
|
||||
<span className="text-[var(--dg-text-muted)] font-mono truncate">{w.object}</span>
|
||||
<span className="text-[var(--dg-text-muted)]">{w.namespace}</span>
|
||||
{w.source && <span className="text-[var(--dg-text-muted)] font-mono">{w.source}</span>}
|
||||
|
||||
@@ -162,6 +162,8 @@ export function KubectlTerminal({ open, onOpenChange, clusterId, clusterName }:
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
// 禁用 Radix Dialog 默认的 ESC 关闭:kubectl 终端会用到 ESC,关闭只能点头部 X 按钮 // Logan.Li
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
className={cn(
|
||||
'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] p-0 gap-0',
|
||||
'!duration-0 !transition-none',
|
||||
|
||||
@@ -98,14 +98,14 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
}, [cluster?.code]);
|
||||
|
||||
return (
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Cloud className="h-6 w-6 text-blue-400" />
|
||||
<Cloud className="h-6 w-6 text-[var(--dg-blue)]" />
|
||||
<div>
|
||||
<CardTitle className="text-slate-100">{cluster.name}</CardTitle>
|
||||
<CardDescription className="text-slate-500">
|
||||
<CardTitle className="text-[var(--dg-text-primary)]">{cluster.name}</CardTitle>
|
||||
<CardDescription className="text-[var(--dg-text-muted)]">
|
||||
{cluster.version} · {cluster.node_count || 0} {t('k8s.cluster.nodeCount')} · {t('k8s.cluster.created')}{' '}
|
||||
{cluster.created_at ? new Date(cluster.created_at).toLocaleDateString() : '-'}
|
||||
</CardDescription>
|
||||
@@ -114,7 +114,7 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-slate-400 hover:text-slate-100"
|
||||
className="text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]"
|
||||
onClick={onClose}
|
||||
>
|
||||
<XCircle className="h-4 w-4 mr-2" />
|
||||
@@ -124,40 +124,40 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="overview" className="w-full">
|
||||
<TabsList className="bg-slate-800 border border-slate-700 flex-wrap h-auto">
|
||||
<TabsTrigger value="overview" className="data-[state=active]:bg-slate-700">
|
||||
<TabsList className="bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] flex-wrap h-auto">
|
||||
<TabsTrigger value="overview" className="data-[state=active]:bg-[var(--dg-bg-active)] data-[state=active]:text-[var(--dg-text-primary)]">
|
||||
{t('k8s.tab.overview')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="nodes" className="data-[state=active]:bg-slate-700">
|
||||
<TabsTrigger value="nodes" className="data-[state=active]:bg-[var(--dg-bg-active)] data-[state=active]:text-[var(--dg-text-primary)]">
|
||||
{t('k8s.tab.nodes')} ({nodes.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="pods" className="data-[state=active]:bg-slate-700">
|
||||
<TabsTrigger value="pods" className="data-[state=active]:bg-[var(--dg-bg-active)] data-[state=active]:text-[var(--dg-text-primary)]">
|
||||
{t('k8s.tab.pods')} ({pods.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="deployments" className="data-[state=active]:bg-slate-700">
|
||||
<TabsTrigger value="deployments" className="data-[state=active]:bg-[var(--dg-bg-active)] data-[state=active]:text-[var(--dg-text-primary)]">
|
||||
{t('k8s.tab.deployments')} ({deployments.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="services" className="data-[state=active]:bg-slate-700">
|
||||
<TabsTrigger value="services" className="data-[state=active]:bg-[var(--dg-bg-active)] data-[state=active]:text-[var(--dg-text-primary)]">
|
||||
{t('k8s.tab.services')} ({services.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="ingresses" className="data-[state=active]:bg-slate-700">
|
||||
<TabsTrigger value="ingresses" className="data-[state=active]:bg-[var(--dg-bg-active)] data-[state=active]:text-[var(--dg-text-primary)]">
|
||||
{t('k8s.tab.ingresses')} ({ingresses.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="events" className="data-[state=active]:bg-slate-700">
|
||||
<TabsTrigger value="events" className="data-[state=active]:bg-[var(--dg-bg-active)] data-[state=active]:text-[var(--dg-text-primary)]">
|
||||
{t('k8s.tab.events')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="mt-4 space-y-4">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<Card className="bg-slate-800/50 border-slate-700">
|
||||
<Card className="bg-[var(--dg-bg-tertiary)]/50 border-[var(--dg-border)]">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<CardTitle className="text-sm text-slate-400">{t('k8s.cluster.resourceTrend')}</CardTitle>
|
||||
<CardTitle className="text-sm text-[var(--dg-text-secondary)]">{t('k8s.cluster.resourceTrend')}</CardTitle>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 text-slate-400 hover:text-slate-200"
|
||||
className="h-6 w-6 p-0 text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]"
|
||||
onClick={() => cluster?.id && loadClusterDetail(cluster.id)}
|
||||
title={t('k8s.cluster.refreshMetrics')}
|
||||
>
|
||||
@@ -171,77 +171,77 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
<AreaChart data={trendData} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
|
||||
<defs>
|
||||
<linearGradient id="colorCpu" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.35} />
|
||||
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0.05} />
|
||||
<stop offset="5%" stopColor="var(--dg-blue)" stopOpacity={0.35} />
|
||||
<stop offset="95%" stopColor="var(--dg-blue)" stopOpacity={0.05} />
|
||||
</linearGradient>
|
||||
<linearGradient id="colorMemory" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#10b981" stopOpacity={0.35} />
|
||||
<stop offset="95%" stopColor="#10b981" stopOpacity={0.05} />
|
||||
<stop offset="5%" stopColor="var(--dg-green)" stopOpacity={0.35} />
|
||||
<stop offset="95%" stopColor="var(--dg-green)" stopOpacity={0.05} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="2 2" stroke="#1e293b" />
|
||||
<XAxis dataKey="time" stroke="#475569" fontSize={11} tickLine={false} />
|
||||
<YAxis stroke="#475569" fontSize={11} domain={[0, 100]} unit="%" tickLine={false} />
|
||||
<CartesianGrid strokeDasharray="2 2" stroke="var(--chart-grid)" />
|
||||
<XAxis dataKey="time" stroke="var(--chart-axis)" fontSize={11} tickLine={false} />
|
||||
<YAxis stroke="var(--chart-axis)" fontSize={11} domain={[0, 100]} unit="%" tickLine={false} />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#0f172a',
|
||||
border: '1px solid #1e293b',
|
||||
backgroundColor: 'var(--chart-tooltip-bg)',
|
||||
border: '1px solid var(--chart-tooltip-border)',
|
||||
borderRadius: '6px',
|
||||
color: '#f8fafc',
|
||||
color: 'var(--dg-text-primary)',
|
||||
fontSize: '12px',
|
||||
}}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="cpu"
|
||||
stroke="#3b82f6"
|
||||
stroke="var(--dg-blue)"
|
||||
strokeWidth={2}
|
||||
fill="url(#colorCpu)"
|
||||
dot={false}
|
||||
activeDot={{ r: 3, fill: '#3b82f6' }}
|
||||
activeDot={{ r: 3, fill: 'var(--dg-blue)' }}
|
||||
name={t('k8s.label.cpuPercent')}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="memory"
|
||||
stroke="#10b981"
|
||||
stroke="var(--dg-green)"
|
||||
strokeWidth={2}
|
||||
fill="url(#colorMemory)"
|
||||
dot={false}
|
||||
activeDot={{ r: 3, fill: '#10b981' }}
|
||||
activeDot={{ r: 3, fill: 'var(--dg-green)' }}
|
||||
name={t('k8s.label.memoryPercent')}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="flex justify-center gap-6 mt-2 text-xs text-slate-400">
|
||||
<span>{t('k8s.label.cpu')}: <span className="text-blue-400 font-bold">{clusterMetrics.cpuPct.toFixed(1)}%</span></span>
|
||||
<span>{t('k8s.label.memory')}: <span className="text-emerald-400 font-bold">{clusterMetrics.memPct.toFixed(1)}%</span></span>
|
||||
<div className="flex justify-center gap-6 mt-2 text-xs text-[var(--dg-text-secondary)]">
|
||||
<span>{t('k8s.label.cpu')}: <span className="text-[var(--dg-blue)] font-bold">{clusterMetrics.cpuPct.toFixed(1)}%</span></span>
|
||||
<span>{t('k8s.label.memory')}: <span className="text-[var(--dg-green)] font-bold">{clusterMetrics.memPct.toFixed(1)}%</span></span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-slate-800/50 border-slate-700">
|
||||
<Card className="bg-[var(--dg-bg-tertiary)]/50 border-[var(--dg-border)]">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm text-slate-400">{t('k8s.cluster.resourceDistribution')}</CardTitle>
|
||||
<CardTitle className="text-sm text-[var(--dg-text-secondary)]">{t('k8s.cluster.resourceDistribution')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="text-center p-4 rounded-lg bg-slate-800">
|
||||
<div className="text-3xl font-bold text-blue-400">{cluster.pod_count || 0}</div>
|
||||
<div className="text-sm text-slate-500 mt-1">{t('k8s.tab.pods')}</div>
|
||||
<div className="text-center p-4 rounded-lg bg-[var(--dg-bg-tertiary)]">
|
||||
<div className="text-3xl font-bold text-[var(--dg-blue)]">{cluster.pod_count || 0}</div>
|
||||
<div className="text-sm text-[var(--dg-text-muted)] mt-1">{t('k8s.tab.pods')}</div>
|
||||
</div>
|
||||
<div className="text-center p-4 rounded-lg bg-slate-800">
|
||||
<div className="text-3xl font-bold text-emerald-400">{cluster.service_count || 0}</div>
|
||||
<div className="text-sm text-slate-500 mt-1">{t('k8s.tab.services')}</div>
|
||||
<div className="text-center p-4 rounded-lg bg-[var(--dg-bg-tertiary)]">
|
||||
<div className="text-3xl font-bold text-[var(--dg-green)]">{cluster.service_count || 0}</div>
|
||||
<div className="text-sm text-[var(--dg-text-muted)] mt-1">{t('k8s.tab.services')}</div>
|
||||
</div>
|
||||
<div className="text-center p-4 rounded-lg bg-slate-800">
|
||||
<div className="text-3xl font-bold text-purple-400">{cluster.deployment_count || 0}</div>
|
||||
<div className="text-sm text-slate-500 mt-1">{t('k8s.tab.deployments')}</div>
|
||||
<div className="text-center p-4 rounded-lg bg-[var(--dg-bg-tertiary)]">
|
||||
<div className="text-3xl font-bold text-[var(--dg-purple)]">{cluster.deployment_count || 0}</div>
|
||||
<div className="text-sm text-[var(--dg-text-muted)] mt-1">{t('k8s.tab.deployments')}</div>
|
||||
</div>
|
||||
<div className="text-center p-4 rounded-lg bg-slate-800">
|
||||
<div className="text-3xl font-bold text-amber-400">{cluster.node_count || 0}</div>
|
||||
<div className="text-sm text-slate-500 mt-1">{t('k8s.tab.nodes')}</div>
|
||||
<div className="text-center p-4 rounded-lg bg-[var(--dg-bg-tertiary)]">
|
||||
<div className="text-3xl font-bold text-[var(--dg-orange)]">{cluster.node_count || 0}</div>
|
||||
<div className="text-sm text-[var(--dg-text-muted)] mt-1">{t('k8s.tab.nodes')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -250,35 +250,35 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="nodes" className="mt-4">
|
||||
<div className="border rounded-lg border-slate-700 overflow-hidden">
|
||||
<div className="border rounded-lg border-[var(--dg-border)] overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-slate-800">
|
||||
<thead className="bg-[var(--dg-bg-tertiary)]">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.name')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.role')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('common.status')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.cpu')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.memory')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.tab.pods')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.os')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.name')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.role')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('common.status')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.cpu')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.memory')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.tab.pods')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.os')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800">
|
||||
<tbody className="divide-y divide-[var(--dg-border)]">
|
||||
{nodes.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-4 py-8 text-center text-slate-500">{t('k8s.empty.nodes')}</td>
|
||||
<td colSpan={7} className="px-4 py-8 text-center text-[var(--dg-text-muted)]">{t('k8s.empty.nodes')}</td>
|
||||
</tr>
|
||||
) : (
|
||||
nodes.map((node) => (
|
||||
<tr key={node.id || node.name} className="hover:bg-slate-800/50">
|
||||
<td className="px-4 py-3 text-sm text-slate-200">{node.name}</td>
|
||||
<tr key={node.id || node.name} className="hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||
<td className="px-4 py-3 text-sm text-[var(--dg-text-primary)]">{node.name}</td>
|
||||
<td className="px-4 py-3">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
node.role === 'master'
|
||||
? 'border-purple-500/30 text-purple-400 bg-purple-500/10'
|
||||
: 'border-blue-500/30 text-blue-400 bg-blue-500/10'
|
||||
? 'border-[var(--dg-purple)]/30 text-[var(--dg-purple)] bg-[var(--dg-purple)]/10'
|
||||
: 'border-[var(--dg-blue)]/30 text-[var(--dg-blue)] bg-[var(--dg-blue)]/10'
|
||||
}
|
||||
>
|
||||
{node.role === 'master' ? 'Master' : 'Worker'}
|
||||
@@ -289,21 +289,21 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
variant="outline"
|
||||
className={
|
||||
(node.status || '').toLowerCase() === 'ready'
|
||||
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
|
||||
: 'border-red-500/30 text-red-400 bg-red-500/10'
|
||||
? 'border-[var(--dg-green)]/30 text-[var(--dg-green)] bg-[var(--dg-green)]/10'
|
||||
: 'border-[var(--dg-red)]/30 text-[var(--dg-red)] bg-[var(--dg-red)]/10'
|
||||
}
|
||||
>
|
||||
{(node.status || '').toLowerCase() === 'ready' ? 'Ready' : 'NotReady'}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-300">
|
||||
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">
|
||||
{node.cpu_usage_m || '0m'} / {node.cpu}{t('k8s.label.cores')}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-300">
|
||||
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">
|
||||
{node.mem_usage_mi || '0Mi'} / {node.memory_mb ? (node.memory_mb / 1024).toFixed(0) + 'GB' : '-'}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-300">{node.max_pods ?? 0}</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-400">{node.os}</td>
|
||||
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{node.max_pods ?? 0}</td>
|
||||
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{node.os}</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
@@ -313,30 +313,30 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="pods" className="mt-4">
|
||||
<div className="border rounded-lg border-slate-700 overflow-hidden">
|
||||
<div className="border rounded-lg border-[var(--dg-border)] overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-slate-800">
|
||||
<thead className="bg-[var(--dg-bg-tertiary)]">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.name')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.namespace')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('common.status')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.restartCount')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.cpu')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.memory')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.node')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.name')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.namespace')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('common.status')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.restartCount')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.cpu')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.memory')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.node')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800">
|
||||
<tbody className="divide-y divide-[var(--dg-border)]">
|
||||
{pods.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-4 py-8 text-center text-slate-500">{t('k8s.empty.pods')}</td>
|
||||
<td colSpan={7} className="px-4 py-8 text-center text-[var(--dg-text-muted)]">{t('k8s.empty.pods')}</td>
|
||||
</tr>
|
||||
) : (
|
||||
pods.map((pod) => (
|
||||
<tr key={pod.id || pod.name} className="hover:bg-slate-800/50">
|
||||
<td className="px-4 py-3 text-sm text-slate-200">{pod.name}</td>
|
||||
<tr key={pod.id || pod.name} className="hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||
<td className="px-4 py-3 text-sm text-[var(--dg-text-primary)]">{pod.name}</td>
|
||||
<td className="px-4 py-3">
|
||||
<Badge variant="outline" className="border-slate-700 text-slate-400">
|
||||
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
|
||||
{pod.namespace}
|
||||
</Badge>
|
||||
</td>
|
||||
@@ -345,19 +345,19 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
variant="outline"
|
||||
className={
|
||||
(pod.status || '').toLowerCase() === 'running'
|
||||
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
|
||||
? 'border-[var(--dg-green)]/30 text-[var(--dg-green)] bg-[var(--dg-green)]/10'
|
||||
: (pod.status || '').toLowerCase() === 'pending'
|
||||
? 'border-amber-500/30 text-amber-400 bg-amber-500/10'
|
||||
: 'border-red-500/30 text-red-400 bg-red-500/10'
|
||||
? 'border-[var(--dg-orange)]/30 text-[var(--dg-orange)] bg-[var(--dg-orange)]/10'
|
||||
: 'border-[var(--dg-red)]/30 text-[var(--dg-red)] bg-[var(--dg-red)]/10'
|
||||
}
|
||||
>
|
||||
{pod.status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-300">{pod.restart_count ?? 0}</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-300">{pod.cpu_usage_m || '0m'}</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-300">{pod.mem_usage_mi || '0Mi'}</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-400">{pod.node_name}</td>
|
||||
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{pod.restart_count ?? 0}</td>
|
||||
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{pod.cpu_usage_m || '0m'}</td>
|
||||
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{pod.mem_usage_mi || '0Mi'}</td>
|
||||
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{pod.node_name}</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
@@ -367,40 +367,40 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="deployments" className="mt-4">
|
||||
<div className="border rounded-lg border-slate-700 overflow-hidden">
|
||||
<div className="border rounded-lg border-[var(--dg-border)] overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-slate-800">
|
||||
<thead className="bg-[var(--dg-bg-tertiary)]">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.name')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.namespace')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.replicas')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.availableReplicas')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.image')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.createdAt')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.name')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.namespace')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.replicas')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.availableReplicas')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.image')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.createdAt')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800">
|
||||
<tbody className="divide-y divide-[var(--dg-border)]">
|
||||
{deployments.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-4 py-8 text-center text-slate-500">{t('k8s.empty.deployments')}</td>
|
||||
<td colSpan={6} className="px-4 py-8 text-center text-[var(--dg-text-muted)]">{t('k8s.empty.deployments')}</td>
|
||||
</tr>
|
||||
) : (
|
||||
deployments.map((deployment) => (
|
||||
<tr key={deployment.id || deployment.name} className="hover:bg-slate-800/50">
|
||||
<td className="px-4 py-3 text-sm text-slate-200">{deployment.name}</td>
|
||||
<tr key={deployment.id || deployment.name} className="hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||
<td className="px-4 py-3 text-sm text-[var(--dg-text-primary)]">{deployment.name}</td>
|
||||
<td className="px-4 py-3">
|
||||
<Badge variant="outline" className="border-slate-700 text-slate-400">
|
||||
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
|
||||
{deployment.namespace}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-300">{deployment.replicas}</td>
|
||||
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{deployment.replicas}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`text-sm ${(deployment.available_replicas ?? deployment.availableReplicas) === deployment.replicas ? 'text-emerald-400' : 'text-amber-400'}`}>
|
||||
<span className={`text-sm ${(deployment.available_replicas ?? deployment.availableReplicas) === deployment.replicas ? 'text-[var(--dg-green)]' : 'text-[var(--dg-orange)]'}`}>
|
||||
{deployment.available_replicas ?? deployment.availableReplicas} / {deployment.replicas}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-400 truncate max-w-[200px]">{deployment.image}</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-400">{deployment.created_at || deployment.createdAt}</td>
|
||||
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)] truncate max-w-[200px]">{deployment.image}</td>
|
||||
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{deployment.created_at || deployment.createdAt}</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
@@ -410,29 +410,29 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="services" className="mt-4">
|
||||
<div className="border rounded-lg border-slate-700 overflow-hidden">
|
||||
<div className="border rounded-lg border-[var(--dg-border)] overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-slate-800">
|
||||
<thead className="bg-[var(--dg-bg-tertiary)]">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.name')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.namespace')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('common.type')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">ClusterIP</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.ports')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.selector')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.name')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.namespace')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('common.type')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">ClusterIP</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.ports')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.selector')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800">
|
||||
<tbody className="divide-y divide-[var(--dg-border)]">
|
||||
{services.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-4 py-8 text-center text-slate-500">{t('k8s.empty.services')}</td>
|
||||
<td colSpan={6} className="px-4 py-8 text-center text-[var(--dg-text-muted)]">{t('k8s.empty.services')}</td>
|
||||
</tr>
|
||||
) : (
|
||||
services.map((service) => (
|
||||
<tr key={service.id || service.name} className="hover:bg-slate-800/50">
|
||||
<td className="px-4 py-3 text-sm text-slate-200">{service.name}</td>
|
||||
<tr key={service.id || service.name} className="hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||
<td className="px-4 py-3 text-sm text-[var(--dg-text-primary)]">{service.name}</td>
|
||||
<td className="px-4 py-3">
|
||||
<Badge variant="outline" className="border-slate-700 text-slate-400">
|
||||
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
|
||||
{service.namespace}
|
||||
</Badge>
|
||||
</td>
|
||||
@@ -441,18 +441,18 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
variant="outline"
|
||||
className={
|
||||
service.type === 'ClusterIP'
|
||||
? 'border-blue-500/30 text-blue-400 bg-blue-500/10'
|
||||
? 'border-[var(--dg-blue)]/30 text-[var(--dg-blue)] bg-[var(--dg-blue)]/10'
|
||||
: service.type === 'NodePort'
|
||||
? 'border-purple-500/30 text-purple-400 bg-purple-500/10'
|
||||
: 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
|
||||
? 'border-[var(--dg-purple)]/30 text-[var(--dg-purple)] bg-[var(--dg-purple)]/10'
|
||||
: 'border-[var(--dg-green)]/30 text-[var(--dg-green)] bg-[var(--dg-green)]/10'
|
||||
}
|
||||
>
|
||||
{service.type}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-300">{service.cluster_ip ?? service.clusterIP}</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-300">{service.ports}</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-400">{service.selector}</td>
|
||||
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{service.cluster_ip ?? service.clusterIP}</td>
|
||||
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{service.ports}</td>
|
||||
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{service.selector}</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
@@ -462,40 +462,40 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="ingresses" className="mt-4">
|
||||
<div className="border rounded-lg border-slate-700 overflow-hidden">
|
||||
<div className="border rounded-lg border-[var(--dg-border)] overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-slate-800">
|
||||
<thead className="bg-[var(--dg-bg-tertiary)]">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.name')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.namespace')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.hosts')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.backend')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.tls')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.name')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.namespace')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.hosts')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.backend')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.tls')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800">
|
||||
<tbody className="divide-y divide-[var(--dg-border)]">
|
||||
{ingresses.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-4 py-8 text-center text-slate-500">{t('k8s.empty.ingresses')}</td>
|
||||
<td colSpan={5} className="px-4 py-8 text-center text-[var(--dg-text-muted)]">{t('k8s.empty.ingresses')}</td>
|
||||
</tr>
|
||||
) : (
|
||||
ingresses.map((ingress) => (
|
||||
<tr key={ingress.id || ingress.name} className="hover:bg-slate-800/50">
|
||||
<td className="px-4 py-3 text-sm text-slate-200">{ingress.name}</td>
|
||||
<tr key={ingress.id || ingress.name} className="hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||
<td className="px-4 py-3 text-sm text-[var(--dg-text-primary)]">{ingress.name}</td>
|
||||
<td className="px-4 py-3">
|
||||
<Badge variant="outline" className="border-slate-700 text-slate-400">
|
||||
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
|
||||
{ingress.namespace}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-300">{ingress.hosts}</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-300">{ingress.backend}</td>
|
||||
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{ingress.hosts}</td>
|
||||
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{ingress.backend}</td>
|
||||
<td className="px-4 py-3">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
ingress.tls
|
||||
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
|
||||
: 'border-slate-700 text-slate-400'
|
||||
? 'border-[var(--dg-green)]/30 text-[var(--dg-green)] bg-[var(--dg-green)]/10'
|
||||
: 'border-[var(--dg-border)] text-[var(--dg-text-secondary)]'
|
||||
}
|
||||
>
|
||||
{ingress.tls ? t('k8s.label.enabled') : t('k8s.label.disabled')}
|
||||
@@ -520,21 +520,21 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
].map((event, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-3 p-3 rounded-lg bg-slate-800/50"
|
||||
className="flex items-center gap-3 p-3 rounded-lg bg-[var(--dg-bg-tertiary)]/50"
|
||||
>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
event.type === 'Normal'
|
||||
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
|
||||
: 'border-amber-500/30 text-amber-400 bg-amber-500/10'
|
||||
? 'border-[var(--dg-green)]/30 text-[var(--dg-green)] bg-[var(--dg-green)]/10'
|
||||
: 'border-[var(--dg-orange)]/30 text-[var(--dg-orange)] bg-[var(--dg-orange)]/10'
|
||||
}
|
||||
>
|
||||
{event.type}
|
||||
</Badge>
|
||||
<span className="text-sm font-medium text-slate-300 min-w-[120px]">{event.reason}</span>
|
||||
<span className="text-sm text-slate-400 flex-1">{event.message}</span>
|
||||
<span className="text-xs text-slate-500">{event.time}</span>
|
||||
<span className="text-sm font-medium text-[var(--dg-text-secondary)] min-w-[120px]">{event.reason}</span>
|
||||
<span className="text-sm text-[var(--dg-text-secondary)] flex-1">{event.message}</span>
|
||||
<span className="text-xs text-[var(--dg-text-muted)]">{event.time}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -135,21 +135,21 @@ export function KubernetesClusters() {
|
||||
switch (status) {
|
||||
case 'healthy':
|
||||
return (
|
||||
<Badge variant="outline" className="border-emerald-500/30 text-emerald-400 bg-emerald-500/10">
|
||||
<Badge variant="outline" className="border-[var(--dg-green)]/30 text-[var(--dg-green)] bg-[var(--dg-green)]/10">
|
||||
<CheckCircle className="h-3 w-3 mr-1" />
|
||||
健康
|
||||
</Badge>
|
||||
);
|
||||
case 'warning':
|
||||
return (
|
||||
<Badge variant="outline" className="border-amber-500/30 text-amber-400 bg-amber-500/10">
|
||||
<Badge variant="outline" className="border-[var(--dg-orange)]/30 text-[var(--dg-orange)] bg-[var(--dg-orange)]/10">
|
||||
<AlertTriangle className="h-3 w-3 mr-1" />
|
||||
警告
|
||||
</Badge>
|
||||
);
|
||||
case 'error':
|
||||
return (
|
||||
<Badge variant="outline" className="border-red-500/30 text-red-400 bg-red-500/10">
|
||||
<Badge variant="outline" className="border-[var(--dg-red)]/30 text-[var(--dg-red)] bg-[var(--dg-red)]/10">
|
||||
<XCircle className="h-3 w-3 mr-1" />
|
||||
异常
|
||||
</Badge>
|
||||
@@ -166,19 +166,19 @@ export function KubernetesClusters() {
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-100">集群列表</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">管理Kubernetes集群资源</p>
|
||||
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)]">集群列表</h1>
|
||||
<p className="text-sm text-[var(--dg-text-muted)] mt-1">管理Kubernetes集群资源</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800"
|
||||
className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]"
|
||||
onClick={loadData}
|
||||
>
|
||||
<RefreshCw className={cn('h-4 w-4 mr-2', loading && 'animate-spin')} />
|
||||
刷新
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={() => { setEditingCluster(null); setDialogOpen(true); }}>
|
||||
<Button className="bg-[var(--dg-blue)] hover:opacity-90 text-white" onClick={() => { setEditingCluster(null); setDialogOpen(true); }}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
添加集群
|
||||
</Button>
|
||||
@@ -188,12 +188,12 @@ export function KubernetesClusters() {
|
||||
{/* Search */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative w-80">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||
<Input
|
||||
placeholder="搜索集群名称或版本..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
|
||||
className="pl-9 h-9 bg-[var(--dg-bg-tertiary)]/50 border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -201,15 +201,15 @@ export function KubernetesClusters() {
|
||||
{/* Clusters Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{loading ? (
|
||||
<Card className="bg-slate-900 border-slate-800 col-span-full">
|
||||
<CardContent className="p-12 text-center text-slate-500">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] col-span-full">
|
||||
<CardContent className="p-12 text-center text-[var(--dg-text-muted)]">
|
||||
<RefreshCw className="h-6 w-6 animate-spin mx-auto mb-3" />
|
||||
加载中...
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : filteredClusters.length === 0 ? (
|
||||
<Card className="bg-slate-900 border-slate-800 col-span-full">
|
||||
<CardContent className="p-12 text-center text-slate-500">暂无集群数据</CardContent>
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] col-span-full">
|
||||
<CardContent className="p-12 text-center text-[var(--dg-text-muted)]">暂无集群数据</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
filteredClusters.map((cluster) => {
|
||||
@@ -220,22 +220,22 @@ export function KubernetesClusters() {
|
||||
<Card
|
||||
key={cluster.id}
|
||||
className={cn(
|
||||
'bg-slate-900 border-slate-800 cursor-pointer transition-all',
|
||||
'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] cursor-pointer transition-all',
|
||||
selectedCluster === cluster.id
|
||||
? 'border-blue-500 ring-1 ring-blue-500/30'
|
||||
: 'hover:border-slate-700'
|
||||
? 'border-[var(--dg-blue)] ring-1 ring-[var(--dg-blue)]/30'
|
||||
: 'hover:border-[var(--dg-border-hover)]'
|
||||
)}
|
||||
onClick={() => setSelectedCluster(cluster.id)}
|
||||
>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center">
|
||||
<Cloud className="h-5 w-5 text-blue-400" />
|
||||
<div className="w-10 h-10 rounded-lg bg-[var(--dg-blue)]/10 flex items-center justify-center">
|
||||
<Cloud className="h-5 w-5 text-[var(--dg-blue)]" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base text-slate-100">{cluster.name}</CardTitle>
|
||||
<CardDescription className="text-slate-500">{cluster.version}</CardDescription>
|
||||
<CardTitle className="text-base text-[var(--dg-text-primary)]">{cluster.name}</CardTitle>
|
||||
<CardDescription className="text-[var(--dg-text-muted)]">{cluster.version}</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -245,21 +245,21 @@ export function KubernetesClusters() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-slate-400 hover:text-slate-100"
|
||||
className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48 bg-slate-800 border-slate-700">
|
||||
<DropdownMenuLabel className="text-slate-300">操作</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className="bg-slate-700" />
|
||||
<DropdownMenuItem className="text-slate-300 focus:bg-slate-700 focus:text-slate-100">
|
||||
<DropdownMenuContent align="end" className="w-48 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
||||
<DropdownMenuLabel className="text-[var(--dg-text-secondary)]">操作</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className="bg-[var(--dg-border-hover)]" />
|
||||
<DropdownMenuItem className="text-[var(--dg-text-secondary)] focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)]">
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
查看详情
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-slate-300 focus:bg-slate-700 focus:text-slate-100"
|
||||
className="text-[var(--dg-text-secondary)] focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditingCluster(cluster);
|
||||
@@ -270,7 +270,7 @@ export function KubernetesClusters() {
|
||||
配置
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-cyan-400 focus:bg-slate-700 focus:text-cyan-300"
|
||||
className="text-[var(--dg-cyan)] focus:bg-[var(--dg-bg-hover)]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSync(cluster.id);
|
||||
@@ -280,7 +280,7 @@ export function KubernetesClusters() {
|
||||
同步
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-emerald-400 focus:bg-slate-700 focus:text-emerald-300"
|
||||
className="text-[var(--dg-green)] focus:bg-[var(--dg-bg-hover)]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setKubectlCluster({ id: cluster.id, name: cluster.name });
|
||||
@@ -291,7 +291,7 @@ export function KubernetesClusters() {
|
||||
kubectl
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-blue-400 focus:bg-slate-700 focus:text-blue-300"
|
||||
className="text-[var(--dg-blue)] focus:bg-[var(--dg-bg-hover)]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setExportCluster(cluster);
|
||||
@@ -300,9 +300,9 @@ export function KubernetesClusters() {
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
导出资源
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="bg-slate-700" />
|
||||
<DropdownMenuSeparator className="bg-[var(--dg-border-hover)]" />
|
||||
<DropdownMenuItem
|
||||
className="text-red-400 focus:bg-slate-700 focus:text-red-400"
|
||||
className="text-[var(--dg-red)] focus:bg-[var(--dg-bg-hover)]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(cluster.id);
|
||||
@@ -318,55 +318,55 @@ export function KubernetesClusters() {
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<div className="grid grid-cols-4 gap-2 mb-4">
|
||||
<div className="text-center p-2 rounded-lg bg-slate-800/50">
|
||||
<div className="text-lg font-semibold text-slate-200">{cluster.ingress_count || 0}</div>
|
||||
<div className="text-xs text-slate-500">Ingress</div>
|
||||
<div className="text-center p-2 rounded-lg bg-[var(--dg-bg-tertiary)]/50">
|
||||
<div className="text-lg font-semibold text-[var(--dg-text-primary)]">{cluster.ingress_count || 0}</div>
|
||||
<div className="text-xs text-[var(--dg-text-muted)]">Ingress</div>
|
||||
</div>
|
||||
<div className="text-center p-2 rounded-lg bg-slate-800/50">
|
||||
<div className="text-lg font-semibold text-slate-200">{cluster.service_count || 0}</div>
|
||||
<div className="text-xs text-slate-500">SVC</div>
|
||||
<div className="text-center p-2 rounded-lg bg-[var(--dg-bg-tertiary)]/50">
|
||||
<div className="text-lg font-semibold text-[var(--dg-text-primary)]">{cluster.service_count || 0}</div>
|
||||
<div className="text-xs text-[var(--dg-text-muted)]">SVC</div>
|
||||
</div>
|
||||
<div className="text-center p-2 rounded-lg bg-slate-800/50">
|
||||
<div className="text-lg font-semibold text-slate-200">{cluster.deployment_count || 0}</div>
|
||||
<div className="text-xs text-slate-500">Deployment</div>
|
||||
<div className="text-center p-2 rounded-lg bg-[var(--dg-bg-tertiary)]/50">
|
||||
<div className="text-lg font-semibold text-[var(--dg-text-primary)]">{cluster.deployment_count || 0}</div>
|
||||
<div className="text-xs text-[var(--dg-text-muted)]">Deployment</div>
|
||||
</div>
|
||||
<div className="text-center p-2 rounded-lg bg-slate-800/50">
|
||||
<div className="text-lg font-semibold text-slate-200">{cluster.pod_count || 0}</div>
|
||||
<div className="text-xs text-slate-500">Pod</div>
|
||||
<div className="text-center p-2 rounded-lg bg-[var(--dg-bg-tertiary)]/50">
|
||||
<div className="text-lg font-semibold text-[var(--dg-text-primary)]">{cluster.pod_count || 0}</div>
|
||||
<div className="text-xs text-[var(--dg-text-muted)]">Pod</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{hasMetrics ? (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-500 w-10">CPU</span>
|
||||
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
|
||||
<span className="text-xs text-[var(--dg-text-muted)] w-10">CPU</span>
|
||||
<div className="flex-1 h-1.5 bg-[var(--dg-bg-tertiary)] rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full',
|
||||
cpuUsage > 80 ? 'bg-red-400' : cpuUsage > 60 ? 'bg-amber-400' : 'bg-emerald-400'
|
||||
cpuUsage > 80 ? 'bg-[var(--dg-red)]' : cpuUsage > 60 ? 'bg-[var(--dg-orange)]' : 'bg-[var(--dg-green)]'
|
||||
)}
|
||||
style={{ width: `${Math.min(cpuUsage, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-slate-400 w-10 text-right">{cpuUsage.toFixed(1)}%</span>
|
||||
<span className="text-xs text-[var(--dg-text-secondary)] w-10 text-right">{cpuUsage.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-500 w-10">内存</span>
|
||||
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
|
||||
<span className="text-xs text-[var(--dg-text-muted)] w-10">内存</span>
|
||||
<div className="flex-1 h-1.5 bg-[var(--dg-bg-tertiary)] rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full',
|
||||
memoryUsage > 80 ? 'bg-red-400' : memoryUsage > 60 ? 'bg-amber-400' : 'bg-emerald-400'
|
||||
memoryUsage > 80 ? 'bg-[var(--dg-red)]' : memoryUsage > 60 ? 'bg-[var(--dg-orange)]' : 'bg-[var(--dg-green)]'
|
||||
)}
|
||||
style={{ width: `${Math.min(memoryUsage, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-slate-400 w-10 text-right">{memoryUsage.toFixed(1)}%</span>
|
||||
<span className="text-xs text-[var(--dg-text-secondary)] w-10 text-right">{memoryUsage.toFixed(1)}%</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-xs text-slate-600 text-center">
|
||||
<div className="text-xs text-[var(--dg-text-muted)] text-center">
|
||||
Ingress {cluster.ingress_count || 0} · SVC {cluster.service_count || 0} · Deployment {cluster.deployment_count || 0} · Pod {cluster.pod_count || 0} · 资源使用率待采集
|
||||
</div>
|
||||
)}
|
||||
@@ -410,13 +410,13 @@ export function KubernetesClusters() {
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
|
||||
onClick={() => { if (!exporting) setExportCluster(null); }}
|
||||
>
|
||||
<div className="bg-slate-900 border border-slate-700 rounded-lg p-5 w-[420px] shadow-xl" onClick={(e) => e.stopPropagation()}>
|
||||
<h3 className="text-base font-semibold text-slate-100 mb-1">导出集群资源</h3>
|
||||
<div className="bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-lg p-5 w-[420px] shadow-xl" onClick={(e) => e.stopPropagation()}>
|
||||
<h3 className="text-base font-semibold text-[var(--dg-text-primary)] mb-1">导出集群资源</h3>
|
||||
{exporting ? (
|
||||
<div className="flex flex-col items-center py-6 gap-3">
|
||||
<Loader2 className="h-9 w-9 animate-spin text-blue-400" />
|
||||
<p className="text-sm text-slate-200">正在导出 {exportCluster.name}</p>
|
||||
<p className="text-xs text-slate-500 text-center leading-relaxed px-2">
|
||||
<Loader2 className="h-9 w-9 animate-spin text-[var(--dg-blue)]" />
|
||||
<p className="text-sm text-[var(--dg-text-primary)]">正在导出 {exportCluster.name}</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] text-center leading-relaxed px-2">
|
||||
正在拉取所有命名空间的 Deployment、Service、Ingress、ConfigMap、Secret 并打包为 tar.gz
|
||||
<br />
|
||||
资源较多时可能需要 1~3 分钟,请稍候勿关闭页面
|
||||
@@ -424,13 +424,13 @@ export function KubernetesClusters() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-xs text-slate-400 mb-4 leading-relaxed">
|
||||
导出 <span className="text-slate-300">{exportCluster.name}</span> 所有命名空间的
|
||||
<p className="text-xs text-[var(--dg-text-secondary)] mb-4 leading-relaxed">
|
||||
导出 <span className="text-[var(--dg-text-primary)]">{exportCluster.name}</span> 所有命名空间的
|
||||
Deployment、Service、Ingress、ConfigMap、Secret 为 tar.gz
|
||||
</p>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => setExportCluster(null)}>取消</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={handleExport}>
|
||||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={() => setExportCluster(null)}>取消</Button>
|
||||
<Button className="bg-[var(--dg-blue)] hover:opacity-90 text-white" onClick={handleExport}>
|
||||
<Download className="h-4 w-4 mr-1.5" />
|
||||
确认导出
|
||||
</Button>
|
||||
|
||||
@@ -50,33 +50,33 @@ export function KubernetesNodes() {
|
||||
|
||||
const getRoleBadge = (role: string) =>
|
||||
role === 'master'
|
||||
? <Badge className="bg-blue-500/20 text-blue-400 border-blue-500/30">Master</Badge>
|
||||
: <Badge className="bg-emerald-500/20 text-emerald-400 border-emerald-500/30">Worker</Badge>;
|
||||
? <Badge className="bg-[var(--dg-blue)]/20 text-[var(--dg-blue)] border-[var(--dg-blue)]/30">Master</Badge>
|
||||
: <Badge className="bg-[var(--dg-green)]/20 text-[var(--dg-green)] border-[var(--dg-green)]/30">Worker</Badge>;
|
||||
|
||||
const getStatusBadge = (status: string) =>
|
||||
status === 'Ready'
|
||||
? <Badge className="bg-emerald-500/20 text-emerald-400 border-emerald-500/30">{status}</Badge>
|
||||
? <Badge className="bg-[var(--dg-green)]/20 text-[var(--dg-green)] border-[var(--dg-green)]/30">{status}</Badge>
|
||||
: <Badge variant="destructive">{status || 'Unknown'}</Badge>;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-100">节点管理</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">Kubernetes 集群工作节点状态与资源</p>
|
||||
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)]">节点管理</h1>
|
||||
<p className="text-sm text-[var(--dg-text-muted)] mt-1">Kubernetes 集群工作节点状态与资源</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={selectedCluster} onValueChange={setSelectedCluster}>
|
||||
<SelectTrigger className="w-48 h-9 bg-slate-800/50 border-slate-700 text-slate-200">
|
||||
<SelectTrigger className="w-48 h-9 bg-[var(--dg-bg-tertiary)]/50 border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<SelectValue placeholder="选择集群" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
||||
{clusters.map((c) => (
|
||||
<SelectItem key={c.id} value={String(c.code || c.id)}>{c.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="icon" className="h-9 w-9 border-slate-700 text-slate-400" onClick={loadNodes} disabled={loading}>
|
||||
<Button variant="outline" size="icon" className="h-9 w-9 border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={loadNodes} disabled={loading}>
|
||||
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -84,14 +84,14 @@ export function KubernetesNodes() {
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative w-56">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
|
||||
<Input placeholder="搜索节点..." value={search} onChange={(e) => setSearch(e.target.value)} className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||
<Input placeholder="搜索节点..." value={search} onChange={(e) => setSearch(e.target.value)} className="pl-9 h-9 bg-[var(--dg-bg-tertiary)]/50 border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
<Select value={roleFilter} onValueChange={setRoleFilter}>
|
||||
<SelectTrigger className="w-32 h-9 bg-slate-800/50 border-slate-700 text-slate-200">
|
||||
<SelectTrigger className="w-32 h-9 bg-[var(--dg-bg-tertiary)]/50 border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
||||
<SelectItem value="all">全部角色</SelectItem>
|
||||
<SelectItem value="master">Master</SelectItem>
|
||||
<SelectItem value="worker">Worker</SelectItem>
|
||||
@@ -99,54 +99,54 @@ export function KubernetesNodes() {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<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">IP</TableHead>
|
||||
<TableHead className="text-slate-400">角色</TableHead>
|
||||
<TableHead className="text-slate-400">状态</TableHead>
|
||||
<TableHead className="text-slate-400">配置</TableHead>
|
||||
<TableHead className="text-slate-400">使用率</TableHead>
|
||||
<TableHead className="text-slate-400">Pods</TableHead>
|
||||
<TableHead className="text-slate-400">Kubelet</TableHead>
|
||||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">节点名称</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">IP</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">角色</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">状态</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">配置</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">使用率</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">Pods</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">Kubelet</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>
|
||||
<TableRow><TableCell colSpan={8} className="text-center text-[var(--dg-text-muted)] py-12"><Loader2 className="h-5 w-5 animate-spin mx-auto mb-2" />加载中...</TableCell></TableRow>
|
||||
) : filtered.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={8} className="text-center text-slate-500 py-12"><Server className="h-8 w-8 mx-auto mb-2 opacity-50" />{selectedCluster ? '暂无节点数据' : '请选择集群'}</TableCell></TableRow>
|
||||
<TableRow><TableCell colSpan={8} className="text-center text-[var(--dg-text-muted)] py-12"><Server className="h-8 w-8 mx-auto mb-2 opacity-50" />{selectedCluster ? '暂无节点数据' : '请选择集群'}</TableCell></TableRow>
|
||||
) : (
|
||||
filtered.map((n) => (
|
||||
<TableRow key={n.id} className="border-slate-800 hover:bg-slate-800/30">
|
||||
<TableCell className="font-medium text-slate-200">{n.name}</TableCell>
|
||||
<TableCell><code className="text-sm text-slate-300 bg-slate-800 px-1.5 py-0.5 rounded">{n.ip || '-'}</code></TableCell>
|
||||
<TableRow key={n.id} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/30">
|
||||
<TableCell className="font-medium text-[var(--dg-text-primary)]">{n.name}</TableCell>
|
||||
<TableCell><code className="text-sm text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)] px-1.5 py-0.5 rounded">{n.ip || '-'}</code></TableCell>
|
||||
<TableCell>{getRoleBadge(n.role)}</TableCell>
|
||||
<TableCell>{getStatusBadge(n.status)}</TableCell>
|
||||
<TableCell className="text-sm text-slate-400">
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">
|
||||
{n.cpu ? `${n.cpu}核` : '-'} / {n.memory ? `${Math.round(n.memory / 1024)}GB` : '-'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{(() => {
|
||||
const cpu = n.cpu_usage || 0;
|
||||
const mem = n.memory_usage || 0;
|
||||
const bar = (val: number) => val >= 85 ? 'bg-red-500' : val >= 60 ? 'bg-yellow-500' : 'bg-emerald-500';
|
||||
const txt = (val: number) => val >= 85 ? 'text-red-400' : val >= 60 ? 'text-yellow-400' : 'text-emerald-400';
|
||||
const bar = (val: number) => val >= 85 ? 'bg-[var(--dg-red)]' : val >= 60 ? 'bg-[var(--dg-orange)]' : 'bg-[var(--dg-green)]';
|
||||
const txt = (val: number) => val >= 85 ? 'text-[var(--dg-red)]' : val >= 60 ? 'text-[var(--dg-orange)]' : 'text-[var(--dg-green)]';
|
||||
return (
|
||||
<div className="flex flex-col gap-1 min-w-[120px]">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-500 w-7">CPU</span>
|
||||
<div className="flex-1 h-1.5 rounded-full bg-slate-700/50 overflow-hidden">
|
||||
<span className="text-xs text-[var(--dg-text-muted)] w-7">CPU</span>
|
||||
<div className="flex-1 h-1.5 rounded-full bg-[var(--dg-bg-tertiary)]/50 overflow-hidden">
|
||||
<div className={`h-full rounded-full ${bar(cpu)}`} style={{ width: `${Math.min(cpu, 100)}%` }} />
|
||||
</div>
|
||||
<span className={`text-xs w-9 text-right ${txt(cpu)}`}>{cpu.toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-500 w-7">Mem</span>
|
||||
<div className="flex-1 h-1.5 rounded-full bg-slate-700/50 overflow-hidden">
|
||||
<span className="text-xs text-[var(--dg-text-muted)] w-7">Mem</span>
|
||||
<div className="flex-1 h-1.5 rounded-full bg-[var(--dg-bg-tertiary)]/50 overflow-hidden">
|
||||
<div className={`h-full rounded-full ${bar(mem)}`} style={{ width: `${Math.min(mem, 100)}%` }} />
|
||||
</div>
|
||||
<span className={`text-xs w-9 text-right ${txt(mem)}`}>{mem.toFixed(0)}%</span>
|
||||
@@ -155,8 +155,8 @@ export function KubernetesNodes() {
|
||||
);
|
||||
})()}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{n.pod_count || 0} / {n.max_pods || '-'}</TableCell>
|
||||
<TableCell className="text-xs text-slate-500">{n.kubelet_version || '-'}</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{n.pod_count || 0} / {n.max_pods || '-'}</TableCell>
|
||||
<TableCell className="text-xs text-[var(--dg-text-muted)]">{n.kubelet_version || '-'}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -108,31 +108,31 @@ export default function NamespaceManagementPage() {
|
||||
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>
|
||||
<div className="flex items-center gap-2 text-sm text-[var(--dg-text-muted)]">
|
||||
<Link to="/kubernetes" className="hover:text-[var(--dg-text-secondary)] transition-colors">集群</Link>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
<Link to={`/kubernetes/${clusterCode}`} className="hover:text-slate-300 transition-colors">{clusterCode}</Link>
|
||||
<Link to={`/kubernetes/${clusterCode}`} className="hover:text-[var(--dg-text-secondary)] transition-colors">{clusterCode}</Link>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
<span className="text-slate-300">命名空间管理</span>
|
||||
<span className="text-[var(--dg-text-secondary)]">命名空间管理</span>
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-100 flex items-center gap-2">
|
||||
<Boxes className="h-6 w-6 text-emerald-400" />
|
||||
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<Boxes className="h-6 w-6 text-[var(--dg-green)]" />
|
||||
命名空间管理
|
||||
</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">命名空间资源配额与生命周期管理</p>
|
||||
<p className="text-sm text-[var(--dg-text-muted)] mt-1">命名空间资源配额与生命周期管理</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"
|
||||
<Button variant="outline" size="icon" className="h-9 w-9 border-[var(--dg-border)] text-[var(--dg-text-secondary)]"
|
||||
onClick={loadData} disabled={loading}>
|
||||
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700">
|
||||
<Button className="bg-[var(--dg-blue)] hover:opacity-90">
|
||||
<Plus className="h-4 w-4 mr-1" />创建命名空间
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
@@ -149,7 +149,7 @@ export default function NamespaceManagementPage() {
|
||||
placeholder="例如: my-app"
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
|
||||
/>
|
||||
<p className="text-xs text-slate-500">
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">
|
||||
名称需符合 DNS-1123 标准(小写字母/数字/连字符)
|
||||
</p>
|
||||
</div>
|
||||
@@ -168,34 +168,34 @@ export default function NamespaceManagementPage() {
|
||||
|
||||
{/* Stats cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-4 flex items-center justify-between">
|
||||
<div><p className="text-xs text-slate-500">命名空间</p><p className="text-xl font-bold text-emerald-400">{namespaces.length}</p></div>
|
||||
<Boxes className="h-6 w-6 opacity-30 text-emerald-400" />
|
||||
<div><p className="text-xs text-[var(--dg-text-muted)]">命名空间</p><p className="text-xl font-bold text-[var(--dg-green)]">{namespaces.length}</p></div>
|
||||
<Boxes className="h-6 w-6 opacity-30 text-[var(--dg-green)]" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-4 flex items-center justify-between">
|
||||
<div><p className="text-xs text-slate-500">Deployments</p><p className="text-xl font-bold text-blue-400">{totalDeploys}</p></div>
|
||||
<Container className="h-6 w-6 opacity-30 text-blue-400" />
|
||||
<div><p className="text-xs text-[var(--dg-text-muted)]">Deployments</p><p className="text-xl font-bold text-[var(--dg-blue)]">{totalDeploys}</p></div>
|
||||
<Container className="h-6 w-6 opacity-30 text-[var(--dg-blue)]" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-4 flex items-center justify-between">
|
||||
<div><p className="text-xs text-slate-500">Pods</p><p className="text-xl font-bold text-purple-400">{totalPods}</p></div>
|
||||
<Activity className="h-6 w-6 opacity-30 text-purple-400" />
|
||||
<div><p className="text-xs text-[var(--dg-text-muted)]">Pods</p><p className="text-xl font-bold text-[var(--dg-purple)]">{totalPods}</p></div>
|
||||
<Activity className="h-6 w-6 opacity-30 text-[var(--dg-purple)]" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-4 flex items-center justify-between">
|
||||
<div><p className="text-xs text-slate-500">Services</p><p className="text-xl font-bold text-cyan-400">{totalSvcs}</p></div>
|
||||
<Network className="h-6 w-6 opacity-30 text-cyan-400" />
|
||||
<div><p className="text-xs text-[var(--dg-text-muted)]">Services</p><p className="text-xl font-bold text-[var(--dg-cyan)]">{totalSvcs}</p></div>
|
||||
<Network className="h-6 w-6 opacity-30 text-[var(--dg-cyan)]" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-4 flex items-center justify-between">
|
||||
<div><p className="text-xs text-slate-500">配额限制</p><p className="text-xl font-bold text-amber-400">{quotaCount}</p></div>
|
||||
<Shield className="h-6 w-6 opacity-30 text-amber-400" />
|
||||
<div><p className="text-xs text-[var(--dg-text-muted)]">配额限制</p><p className="text-xl font-bold text-[var(--dg-orange)]">{quotaCount}</p></div>
|
||||
<Shield className="h-6 w-6 opacity-30 text-[var(--dg-orange)]" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -206,89 +206,89 @@ export default function NamespaceManagementPage() {
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="搜索命名空间..."
|
||||
className="max-w-xs bg-slate-900 border-slate-700"
|
||||
className="max-w-xs bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Namespace table */}
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<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 text-center">Deploy</TableHead>
|
||||
<TableHead className="text-slate-400 text-center">Pods</TableHead>
|
||||
<TableHead className="text-slate-400 text-center">Svc</TableHead>
|
||||
<TableHead className="text-slate-400 text-center">配额</TableHead>
|
||||
<TableHead className="text-slate-400">标签</TableHead>
|
||||
<TableHead className="text-slate-400 text-center">操作</TableHead>
|
||||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">名称</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">状态</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">年龄</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)] text-center">Deploy</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)] text-center">Pods</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)] text-center">Svc</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)] text-center">配额</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">标签</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)] text-center">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow><TableCell colSpan={9} className="text-center text-slate-500 py-12">
|
||||
<TableRow><TableCell colSpan={9} className="text-center text-[var(--dg-text-muted)] py-12">
|
||||
<Loader2 className="h-5 w-5 animate-spin mx-auto mb-2" />加载中...
|
||||
</TableCell></TableRow>
|
||||
) : filtered.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={9} className="text-center text-slate-500 py-12">
|
||||
<TableRow><TableCell colSpan={9} className="text-center text-[var(--dg-text-muted)] py-12">
|
||||
<Boxes className="h-8 w-8 mx-auto mb-2 opacity-50" />暂无命名空间
|
||||
</TableCell></TableRow>
|
||||
) : (
|
||||
filtered.map((ns) => {
|
||||
const isSystem = SYSTEM_NAMESPACES.has(ns.name);
|
||||
return (
|
||||
<TableRow key={ns.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||
<TableRow key={ns.name} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/30">
|
||||
<TableCell className="font-medium">
|
||||
<button
|
||||
className="text-blue-400 hover:text-blue-300 hover:underline text-left"
|
||||
className="text-[var(--dg-blue)] hover:text-[var(--dg-blue)] hover:underline text-left"
|
||||
onClick={() => navigate(`/kubernetes/${clusterCode}/namespace/${ns.name}`)}
|
||||
>
|
||||
{ns.name}
|
||||
</button>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={ns.status === 'Active' ? 'bg-emerald-500/20 text-emerald-400' : 'bg-red-500/20 text-red-400'}>
|
||||
<Badge className={ns.status === 'Active' ? 'bg-[var(--dg-green)]/20 text-[var(--dg-green)]' : 'bg-[var(--dg-red)]/20 text-[var(--dg-red)]'}>
|
||||
{ns.status === 'Active' ? <CheckCircle2 className="h-3 w-3 mr-1" /> : <AlertTriangle className="h-3 w-3 mr-1" />}
|
||||
{ns.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{ns.age}</TableCell>
|
||||
<TableCell className="text-center text-sm text-slate-300">{ns.resource_counts?.deployments ?? 0}</TableCell>
|
||||
<TableCell className="text-center text-sm text-slate-300">{ns.resource_counts?.pods ?? 0}</TableCell>
|
||||
<TableCell className="text-center text-sm text-slate-300">{ns.resource_counts?.services ?? 0}</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{ns.age}</TableCell>
|
||||
<TableCell className="text-center text-sm text-[var(--dg-text-secondary)]">{ns.resource_counts?.deployments ?? 0}</TableCell>
|
||||
<TableCell className="text-center text-sm text-[var(--dg-text-secondary)]">{ns.resource_counts?.pods ?? 0}</TableCell>
|
||||
<TableCell className="text-center text-sm text-[var(--dg-text-secondary)]">{ns.resource_counts?.services ?? 0}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{ns.has_resource_quota ? (
|
||||
<Badge className="bg-amber-500/20 text-amber-400" title={JSON.stringify(ns.resource_quotas?.[0]?.status)}>
|
||||
<Badge className="bg-[var(--dg-orange)]/20 text-[var(--dg-orange)]" title={JSON.stringify(ns.resource_quotas?.[0]?.status)}>
|
||||
<Shield className="h-3 w-3 mr-1" />有
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-xs text-slate-600">-</span>
|
||||
<span className="text-xs text-[var(--dg-text-muted)]">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{ns.labels && Object.keys(ns.labels).length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1 max-w-[200px]">
|
||||
{Object.entries(ns.labels).slice(0, 3).map(([k, v]) => (
|
||||
<span key={k} className="px-1.5 py-0.5 bg-slate-800 rounded text-slate-400">{k}={v}</span>
|
||||
<span key={k} className="px-1.5 py-0.5 bg-[var(--dg-bg-tertiary)] rounded text-[var(--dg-text-secondary)]">{k}={v}</span>
|
||||
))}
|
||||
{Object.keys(ns.labels).length > 3 && (
|
||||
<span className="text-slate-600">+{Object.keys(ns.labels).length - 3}</span>
|
||||
<span className="text-[var(--dg-text-muted)]">+{Object.keys(ns.labels).length - 3}</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-slate-600">-</span>
|
||||
<span className="text-[var(--dg-text-muted)]">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{isSystem ? (
|
||||
<span className="text-xs text-slate-600">系统</span>
|
||||
<span className="text-xs text-[var(--dg-text-muted)]">系统</span>
|
||||
) : (
|
||||
<Button
|
||||
variant="ghost" size="icon"
|
||||
className="h-7 w-7 text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||
className="h-7 w-7 text-[var(--dg-red)] hover:text-[var(--dg-red)] hover:bg-[var(--dg-red)]/10"
|
||||
onClick={() => setDeleteTarget(ns.name)}
|
||||
title="删除命名空间"
|
||||
>
|
||||
@@ -309,16 +309,16 @@ export default function NamespaceManagementPage() {
|
||||
<Dialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-red-400 flex items-center gap-2">
|
||||
<DialogTitle className="text-[var(--dg-red)] flex items-center gap-2">
|
||||
<AlertTriangle className="h-5 w-5" />删除命名空间
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 py-4">
|
||||
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-3">
|
||||
<p className="text-sm text-red-300">
|
||||
<div className="bg-[var(--dg-red)]/10 border border-[var(--dg-red)]/30 rounded-lg p-3">
|
||||
<p className="text-sm text-[var(--dg-red)]">
|
||||
⚠️ 即将删除命名空间 <span className="font-bold">{deleteTarget}</span> 及其中所有资源。
|
||||
</p>
|
||||
<p className="text-xs text-red-400/70 mt-1">
|
||||
<p className="text-xs text-[var(--dg-red)]/70 mt-1">
|
||||
此操作不可逆,所有 Deployment/Pod/Service/ConfigMap 等资源将被永久删除。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -65,22 +65,22 @@ export default function NamespaceOverviewPage() {
|
||||
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>
|
||||
<div className="flex items-center gap-2 text-sm text-[var(--dg-text-muted)]">
|
||||
<Link to="/kubernetes" className="hover:text-[var(--dg-text-primary)] transition-colors">集群</Link>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
<Link to={`/kubernetes/${clusterCode}`} className="hover:text-slate-300 transition-colors">{clusterCode}</Link>
|
||||
<Link to={`/kubernetes/${clusterCode}`} className="hover:text-[var(--dg-text-primary)] transition-colors">{clusterCode}</Link>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
<span className="text-slate-300">{namespace}</span>
|
||||
<span className="text-[var(--dg-text-primary)]">{namespace}</span>
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-100 flex items-center gap-2">
|
||||
<Cloud className="h-6 w-6 text-blue-400" />
|
||||
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<Cloud className="h-6 w-6 text-[var(--dg-blue)]" />
|
||||
{namespace}
|
||||
</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">命名空间概览</p>
|
||||
<p className="text-sm text-[var(--dg-text-muted)] mt-1">命名空间概览</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -89,15 +89,15 @@ export default function NamespaceOverviewPage() {
|
||||
{RESOURCE_LINKS.map((r) => (
|
||||
<Link key={r.type}
|
||||
to={`/kubernetes/${clusterCode}/namespace/${namespace}/${r.type}`}
|
||||
className="bg-slate-900 border border-slate-800 rounded-lg p-4 hover:border-slate-600 hover:bg-slate-800/50 transition-all group"
|
||||
className="bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-lg p-4 hover:border-[var(--dg-border-hover)] hover:bg-[var(--dg-bg-tertiary)]/50 transition-all group"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<r.icon className="h-5 w-5 text-slate-500 group-hover:text-blue-400 transition-colors" />
|
||||
<ChevronRight className="h-4 w-4 text-slate-600 group-hover:text-slate-400 transition-colors" />
|
||||
<r.icon className="h-5 w-5 text-[var(--dg-text-muted)] group-hover:text-[var(--dg-blue)] transition-colors" />
|
||||
<ChevronRight className="h-4 w-4 text-[var(--dg-text-muted)] group-hover:text-[var(--dg-text-secondary)] transition-colors" />
|
||||
</div>
|
||||
<div className="text-lg font-bold text-slate-100">{loading ? '-' : (counts[r.type] ?? 0)}</div>
|
||||
<div className="text-sm text-slate-500">{r.label}</div>
|
||||
<div className="text-xs text-slate-600 mt-1">{r.desc}</div>
|
||||
<div className="text-lg font-bold text-[var(--dg-text-primary)]">{loading ? '-' : (counts[r.type] ?? 0)}</div>
|
||||
<div className="text-sm text-[var(--dg-text-muted)]">{r.label}</div>
|
||||
<div className="text-xs text-[var(--dg-text-muted)] mt-1">{r.desc}</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -42,9 +42,9 @@ const BULK_EXPORT_TYPES: ResourceType[] = ['services', 'ingresses', 'configmaps'
|
||||
|
||||
const VisualActions = ({ name, opts }: { name: string; opts: { onYaml?: (n: string) => void; onEdit?: (n: string) => void } }) => (
|
||||
<div className="flex items-center gap-0.5">
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-emerald-400 hover:text-emerald-300" title="可视化编辑"
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-[var(--dg-green)] hover:text-[var(--dg-green)]" title="可视化编辑"
|
||||
onClick={() => opts.onEdit?.(name)}><Pencil className="h-3.5 w-3.5 mr-1" />编辑</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-slate-400 hover:text-slate-300" title="YAML"
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-secondary)]" title="YAML"
|
||||
onClick={() => opts.onYaml?.(name)}><FileCode className="h-3.5 w-3.5 mr-1" />YAML</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -60,24 +60,24 @@ const RESOURCE_CONFIG: Record<ResourceType, {
|
||||
title: 'Deployments', subtitle: 'Deployment 工作负载运行状态', icon: Boxes,
|
||||
columns: ['名称', '命名空间', '副本', '镜像'],
|
||||
renderRow: (d, opts) => (
|
||||
<TableRow key={d.name} className="border-slate-800 hover:bg-slate-800/30 cursor-pointer" onClick={() => opts.onDetail?.(d)}>
|
||||
<TableCell className="font-medium text-slate-200">{d.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{d.namespace}</Badge></TableCell>
|
||||
<TableRow key={d.name} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/30 cursor-pointer" onClick={() => opts.onDetail?.(d)}>
|
||||
<TableCell className="font-medium text-[var(--dg-text-primary)]">{d.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">{d.namespace}</Badge></TableCell>
|
||||
<TableCell>
|
||||
{(() => {
|
||||
const ready = d.ready_replicas || 0; const total = d.replicas || 0;
|
||||
const pct = total > 0 ? (ready / total) * 100 : 0; const done = ready === total;
|
||||
return (
|
||||
<div className="flex items-center gap-2 min-w-[100px]">
|
||||
<div className="flex-1 h-1.5 rounded-full bg-slate-700/50 overflow-hidden">
|
||||
<div className={`h-full rounded-full ${done ? 'bg-emerald-500' : 'bg-yellow-500'}`} style={{ width: `${pct}%` }} />
|
||||
<div className="flex-1 h-1.5 rounded-full bg-[var(--dg-bg-tertiary)]/50 overflow-hidden">
|
||||
<div className={`h-full rounded-full ${done ? 'bg-[var(--dg-green)]' : 'bg-[var(--dg-orange)]'}`} style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
<span className={`text-xs ${done ? 'text-emerald-400' : 'text-yellow-400'}`}>{ready}/{total}</span>
|
||||
<span className={`text-xs ${done ? 'text-[var(--dg-green)]' : 'text-[var(--dg-orange)]'}`}>{ready}/{total}</span>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-slate-500 max-w-[200px] truncate">{d.image || '-'}</TableCell>
|
||||
<TableCell className="text-xs text-[var(--dg-text-muted)] max-w-[200px] truncate">{d.image || '-'}</TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
},
|
||||
@@ -85,31 +85,31 @@ const RESOURCE_CONFIG: Record<ResourceType, {
|
||||
title: 'Pods', subtitle: 'Pod 实例运行状态', icon: Container,
|
||||
columns: ['名称', '命名空间', '状态', '节点', 'CPU', '内存', '重启', '镜像', '操作'],
|
||||
renderRow: (p, opts) => {
|
||||
const statusMap: Record<string, string> = { Running: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30', Pending: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30', Failed: 'bg-red-500/20 text-red-400 border-red-500/30' };
|
||||
const statusMap: Record<string, string> = { Running: 'bg-[var(--dg-green)]/20 text-[var(--dg-green)] border-[var(--dg-green)]/30', Pending: 'bg-[var(--dg-orange)]/20 text-[var(--dg-orange)] border-[var(--dg-orange)]/30', Failed: 'bg-[var(--dg-red)]/20 text-[var(--dg-red)] border-[var(--dg-red)]/30' };
|
||||
return (
|
||||
<TableRow key={p.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||
<TableCell className="font-medium text-slate-200 cursor-pointer hover:text-[var(--dg-blue)]" onClick={() => opts.onPodDetail?.(p)}>
|
||||
<TableRow key={p.name} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/30">
|
||||
<TableCell className="font-medium text-[var(--dg-text-primary)] cursor-pointer hover:text-[var(--dg-blue)]" onClick={() => opts.onPodDetail?.(p)}>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Eye className="h-3.5 w-3.5 text-[var(--dg-text-muted)] shrink-0" />
|
||||
<span className="truncate">{p.name}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{p.namespace}</Badge></TableCell>
|
||||
<TableCell><Badge className={statusMap[p.status] || 'bg-slate-500/20 text-slate-400'}>{p.status || 'Unknown'}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{p.node_name || '-'}</TableCell>
|
||||
<TableCell className="text-sm text-cyan-400">{p.cpu_usage_m || '-'}</TableCell>
|
||||
<TableCell className="text-sm text-amber-400">{p.mem_usage_mi || '-'}</TableCell>
|
||||
<TableCell className={(p.restart_count > 0 ? 'text-yellow-400' : 'text-slate-500')}>{p.restart_count || 0}</TableCell>
|
||||
<TableCell className="text-xs text-slate-500 max-w-[200px] truncate">{p.image || '-'}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">{p.namespace}</Badge></TableCell>
|
||||
<TableCell><Badge className={statusMap[p.status] || 'bg-[var(--dg-text-muted)]/20 text-[var(--dg-text-secondary)]'}>{p.status || 'Unknown'}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{p.node_name || '-'}</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-cyan)]">{p.cpu_usage_m || '-'}</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-orange)]">{p.mem_usage_mi || '-'}</TableCell>
|
||||
<TableCell className={(p.restart_count > 0 ? 'text-[var(--dg-orange)]' : 'text-[var(--dg-text-muted)]')}>{p.restart_count || 0}</TableCell>
|
||||
<TableCell className="text-xs text-[var(--dg-text-muted)] max-w-[200px] truncate">{p.image || '-'}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<Button variant="ghost" size="sm" className="h-7 w-7 p-0 shrink-0 text-emerald-400" title="终端"
|
||||
<Button variant="ghost" size="sm" className="h-7 w-7 p-0 shrink-0 text-[var(--dg-green)]" title="终端"
|
||||
onClick={() => (opts as Record<string, any>).onTerminal?.(p)}><Terminal className="h-3.5 w-3.5" /></Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 w-7 p-0 shrink-0 text-amber-400" title="文件"
|
||||
<Button variant="ghost" size="sm" className="h-7 w-7 p-0 shrink-0 text-[var(--dg-orange)]" title="文件"
|
||||
onClick={() => (opts as Record<string, any>).onFileBrowser?.(p)}><HardDrive className="h-3.5 w-3.5" /></Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 w-7 p-0 shrink-0 text-blue-400" title="YAML"
|
||||
<Button variant="ghost" size="sm" className="h-7 w-7 p-0 shrink-0 text-[var(--dg-blue)]" title="YAML"
|
||||
onClick={() => opts.onYaml?.(p.name)}><FileCode className="h-3.5 w-3.5" /></Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 w-7 p-0 shrink-0 text-red-400 hover:text-red-300" title="删除"
|
||||
<Button variant="ghost" size="sm" className="h-7 w-7 p-0 shrink-0 text-[var(--dg-red)] hover:text-[var(--dg-red)]" title="删除"
|
||||
onClick={() => opts.onDeletePod?.(p)}><Trash2 className="h-3.5 w-3.5" /></Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -121,12 +121,12 @@ const RESOURCE_CONFIG: Record<ResourceType, {
|
||||
title: 'Services', subtitle: 'Service 网络服务', icon: Network,
|
||||
columns: ['名称', '命名空间', '类型', 'Cluster IP', '端口', '操作'],
|
||||
renderRow: (s, opts) => (
|
||||
<TableRow key={s.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||
<TableCell className="font-medium text-slate-200">{s.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{s.namespace}</Badge></TableCell>
|
||||
<TableCell><Badge className="bg-blue-500/20 text-blue-400 border-blue-500/30">{s.type || '-'}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{s.cluster_ip || '-'}</TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{s.ports || '-'}</TableCell>
|
||||
<TableRow key={s.name} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/30">
|
||||
<TableCell className="font-medium text-[var(--dg-text-primary)]">{s.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">{s.namespace}</Badge></TableCell>
|
||||
<TableCell><Badge className="bg-[var(--dg-blue)]/20 text-[var(--dg-blue)] border-[var(--dg-blue)]/30">{s.type || '-'}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{s.cluster_ip || '-'}</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{s.ports || '-'}</TableCell>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}><VisualActions name={s.name} opts={opts} /></TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
@@ -137,11 +137,11 @@ const RESOURCE_CONFIG: Record<ResourceType, {
|
||||
renderRow: (i, opts) => {
|
||||
const hosts = i.hosts || [];
|
||||
return (
|
||||
<TableRow key={i.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||
<TableCell className="font-medium text-slate-200">{i.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{i.namespace}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-slate-400 max-w-[250px] truncate">{hosts.length > 0 ? hosts.join(', ') : '-'}</TableCell>
|
||||
<TableCell><Badge className={i.tls ? 'bg-emerald-500/20 text-emerald-400' : 'bg-slate-500/20 text-slate-400'}>{i.tls ? 'HTTPS' : 'HTTP'}</Badge></TableCell>
|
||||
<TableRow key={i.name} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/30">
|
||||
<TableCell className="font-medium text-[var(--dg-text-primary)]">{i.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">{i.namespace}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)] max-w-[250px] truncate">{hosts.length > 0 ? hosts.join(', ') : '-'}</TableCell>
|
||||
<TableCell><Badge className={i.tls ? 'bg-[var(--dg-green)]/20 text-[var(--dg-green)]' : 'bg-[var(--dg-text-muted)]/20 text-[var(--dg-text-secondary)]'}>{i.tls ? 'HTTPS' : 'HTTP'}</Badge></TableCell>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}><VisualActions name={i.name} opts={opts} /></TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
@@ -151,10 +151,10 @@ const RESOURCE_CONFIG: Record<ResourceType, {
|
||||
title: 'ConfigMaps', subtitle: '配置字典', icon: FileText,
|
||||
columns: ['名称', '命名空间', '键数量', '操作'],
|
||||
renderRow: (c, opts) => (
|
||||
<TableRow key={c.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||
<TableCell className="font-medium text-slate-200">{c.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{c.namespace}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{c.key_count || '-'}</TableCell>
|
||||
<TableRow key={c.name} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/30">
|
||||
<TableCell className="font-medium text-[var(--dg-text-primary)]">{c.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">{c.namespace}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{c.key_count || '-'}</TableCell>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}><VisualActions name={c.name} opts={opts} /></TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
@@ -163,10 +163,10 @@ const RESOURCE_CONFIG: Record<ResourceType, {
|
||||
title: 'Secrets', subtitle: '密钥管理(仅名称与类型)', icon: Shield,
|
||||
columns: ['名称', '命名空间', '类型', '操作'],
|
||||
renderRow: (s, opts) => (
|
||||
<TableRow key={s.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||
<TableCell className="font-medium text-slate-200">{s.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{s.namespace}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{s.type || '-'}</TableCell>
|
||||
<TableRow key={s.name} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/30">
|
||||
<TableCell className="font-medium text-[var(--dg-text-primary)]">{s.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">{s.namespace}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{s.type || '-'}</TableCell>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}><VisualActions name={s.name} opts={opts} /></TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
@@ -175,13 +175,13 @@ const RESOURCE_CONFIG: Record<ResourceType, {
|
||||
title: 'HPAs', subtitle: '水平自动伸缩', icon: Activity,
|
||||
columns: ['名称', '命名空间', '目标', '最小/最大', 'CPU%', 'Mem%'],
|
||||
renderRow: (h, _opts) => (
|
||||
<TableRow key={h.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||
<TableCell className="font-medium text-slate-200">{h.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{h.namespace}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{h.target_kind}/{h.target_name}</TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{h.min_replicas}/{h.max_replicas}</TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{h.cpu_percent ?? '-'}%</TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{h.mem_percent ?? '-'}%</TableCell>
|
||||
<TableRow key={h.name} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/30">
|
||||
<TableCell className="font-medium text-[var(--dg-text-primary)]">{h.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">{h.namespace}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{h.target_kind}/{h.target_name}</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{h.min_replicas}/{h.max_replicas}</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{h.cpu_percent ?? '-'}%</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{h.mem_percent ?? '-'}%</TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
},
|
||||
@@ -189,12 +189,12 @@ const RESOURCE_CONFIG: Record<ResourceType, {
|
||||
title: 'CronJobs', subtitle: '定时任务', icon: Clock,
|
||||
columns: ['名称', '命名空间', '调度', '状态', '上次执行'],
|
||||
renderRow: (cj) => (
|
||||
<TableRow key={cj.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||
<TableCell className="font-medium text-slate-200">{cj.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{cj.namespace}</Badge></TableCell>
|
||||
<TableCell className="text-sm font-mono text-slate-400">{cj.schedule || '-'}</TableCell>
|
||||
<TableCell><Badge className={cj.suspend ? 'bg-yellow-500/20 text-yellow-400' : 'bg-emerald-500/20 text-emerald-400'}>{cj.suspend ? '已暂停' : '运行中'}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{cj.last_schedule || '-'}</TableCell>
|
||||
<TableRow key={cj.name} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/30">
|
||||
<TableCell className="font-medium text-[var(--dg-text-primary)]">{cj.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">{cj.namespace}</Badge></TableCell>
|
||||
<TableCell className="text-sm font-mono text-[var(--dg-text-secondary)]">{cj.schedule || '-'}</TableCell>
|
||||
<TableCell><Badge className={cj.suspend ? 'bg-[var(--dg-orange)]/20 text-[var(--dg-orange)]' : 'bg-[var(--dg-green)]/20 text-[var(--dg-green)]'}>{cj.suspend ? '已暂停' : '运行中'}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{cj.last_schedule || '-'}</TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
},
|
||||
@@ -202,12 +202,12 @@ const RESOURCE_CONFIG: Record<ResourceType, {
|
||||
title: '存储卷声明', subtitle: 'PersistentVolumeClaim', icon: Boxes,
|
||||
columns: ['名称', '命名空间', '状态', '大小', 'StorageClass', '操作'],
|
||||
renderRow: (p, opts) => (
|
||||
<TableRow key={p.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||
<TableCell className="font-medium text-slate-200">{p.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{p.namespace}</Badge></TableCell>
|
||||
<TableCell><Badge className={p.phase === 'Bound' ? 'bg-emerald-500/20 text-emerald-400' : 'bg-yellow-500/20 text-yellow-400'}>{p.phase || '-'}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{p.size || '-'}</TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{p.storage_class || '-'}</TableCell>
|
||||
<TableRow key={p.name} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/30">
|
||||
<TableCell className="font-medium text-[var(--dg-text-primary)]">{p.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">{p.namespace}</Badge></TableCell>
|
||||
<TableCell><Badge className={p.phase === 'Bound' ? 'bg-[var(--dg-green)]/20 text-[var(--dg-green)]' : 'bg-[var(--dg-orange)]/20 text-[var(--dg-orange)]'}>{p.phase || '-'}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{p.size || '-'}</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{p.storage_class || '-'}</TableCell>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}><VisualActions name={p.name} opts={opts} /></TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
@@ -216,12 +216,12 @@ const RESOURCE_CONFIG: Record<ResourceType, {
|
||||
title: 'StatefulSets', subtitle: '有状态应用', icon: Boxes,
|
||||
columns: ['名称', '命名空间', '副本', '就绪', '镜像'],
|
||||
renderRow: (s, opts) => (
|
||||
<TableRow key={s.name} className="border-slate-800 hover:bg-slate-800/30 cursor-pointer" onClick={() => opts.onDetail?.(s)}>
|
||||
<TableCell className="font-medium text-slate-200">{s.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{s.namespace}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{s.replicas || 0}</TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{s.ready || 0}</TableCell>
|
||||
<TableCell className="text-xs text-slate-500 max-w-[200px] truncate">{s.image || '-'}</TableCell>
|
||||
<TableRow key={s.name} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/30 cursor-pointer" onClick={() => opts.onDetail?.(s)}>
|
||||
<TableCell className="font-medium text-[var(--dg-text-primary)]">{s.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">{s.namespace}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{s.replicas || 0}</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{s.ready || 0}</TableCell>
|
||||
<TableCell className="text-xs text-[var(--dg-text-muted)] max-w-[200px] truncate">{s.image || '-'}</TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
},
|
||||
@@ -229,13 +229,13 @@ const RESOURCE_CONFIG: Record<ResourceType, {
|
||||
title: 'DaemonSets', subtitle: '守护进程集', icon: Boxes,
|
||||
columns: ['名称', '命名空间', '期望', '当前', '就绪', '镜像'],
|
||||
renderRow: (d, opts) => (
|
||||
<TableRow key={d.name} className="border-slate-800 hover:bg-slate-800/30 cursor-pointer" onClick={() => opts.onDetail?.(d)}>
|
||||
<TableCell className="font-medium text-slate-200">{d.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{d.namespace}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{d.desired || 0}</TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{d.current || 0}</TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{d.ready || 0}</TableCell>
|
||||
<TableCell className="text-xs text-slate-500 max-w-[200px] truncate">{d.image || '-'}</TableCell>
|
||||
<TableRow key={d.name} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/30 cursor-pointer" onClick={() => opts.onDetail?.(d)}>
|
||||
<TableCell className="font-medium text-[var(--dg-text-primary)]">{d.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">{d.namespace}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{d.desired || 0}</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{d.current || 0}</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{d.ready || 0}</TableCell>
|
||||
<TableCell className="text-xs text-[var(--dg-text-muted)] max-w-[200px] truncate">{d.image || '-'}</TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
},
|
||||
@@ -243,16 +243,16 @@ const RESOURCE_CONFIG: Record<ResourceType, {
|
||||
title: 'ServiceAccounts', subtitle: '服务账号(RBAC 身份标识)', icon: Shield,
|
||||
columns: ['名称', '命名空间', 'Secret数', '镜像拉取凭证', '创建时间', '操作'],
|
||||
renderRow: (sa, opts) => (
|
||||
<TableRow key={sa.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||
<TableCell className="font-medium text-slate-200">{sa.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{sa.namespace}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{sa.secret_count ?? 0}</TableCell>
|
||||
<TableCell className="text-xs text-slate-500 max-w-[200px] truncate">
|
||||
<TableRow key={sa.name} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/30">
|
||||
<TableCell className="font-medium text-[var(--dg-text-primary)]">{sa.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">{sa.namespace}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{sa.secret_count ?? 0}</TableCell>
|
||||
<TableCell className="text-xs text-[var(--dg-text-muted)] max-w-[200px] truncate">
|
||||
{sa.image_pull_secrets && sa.image_pull_secrets.length > 0 ? sa.image_pull_secrets.join(', ') : '-'}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-slate-500">{sa.age || '-'}</TableCell>
|
||||
<TableCell className="text-xs text-[var(--dg-text-muted)]">{sa.age || '-'}</TableCell>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-blue-400" title="查看YAML"
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-text-secondary)] hover:text-[var(--dg-blue)]" title="查看YAML"
|
||||
onClick={() => opts.onYaml?.(sa.name)}><FileCode className="h-3.5 w-3.5" /></Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -267,22 +267,22 @@ const RESOURCE_CONFIG: Record<ResourceType, {
|
||||
const verbsStr = verbs.length > 4 ? verbs.slice(0, 4).join(', ') + ` +${verbs.length - 4}` : verbs.join(', ');
|
||||
const resStr = resources.length > 4 ? resources.slice(0, 4).join(', ') + ` +${resources.length - 4}` : resources.join(', ');
|
||||
return (
|
||||
<TableRow key={r.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||
<TableCell className="font-medium text-slate-200">{r.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{r.namespace}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{r.rule_count ?? 0}</TableCell>
|
||||
<TableCell className="text-xs text-slate-500 max-w-[280px]">
|
||||
<TableRow key={r.name} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/30">
|
||||
<TableCell className="font-medium text-[var(--dg-text-primary)]">{r.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">{r.namespace}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{r.rule_count ?? 0}</TableCell>
|
||||
<TableCell className="text-xs text-[var(--dg-text-muted)] max-w-[280px]">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-cyan-400 truncate" title={verbs.join(', ')}>verbs: {verbsStr || '-'}</span>
|
||||
<span className="text-amber-400 truncate" title={resources.join(', ')}>resources: {resStr || '-'}</span>
|
||||
<span className="text-[var(--dg-cyan)] truncate" title={verbs.join(', ')}>verbs: {verbsStr || '-'}</span>
|
||||
<span className="text-[var(--dg-orange)] truncate" title={resources.join(', ')}>resources: {resStr || '-'}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-slate-500">{r.age || '-'}</TableCell>
|
||||
<TableCell className="text-xs text-[var(--dg-text-muted)]">{r.age || '-'}</TableCell>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-blue-400" title="查看YAML"
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-text-secondary)] hover:text-[var(--dg-blue)]" title="查看YAML"
|
||||
onClick={() => opts.onYaml?.(r.name)}><FileCode className="h-3.5 w-3.5" /></Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-emerald-400 hover:text-emerald-300" title="编辑"
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-green)] hover:text-[var(--dg-green)]" title="编辑"
|
||||
onClick={() => opts.onEdit?.(r.name)}><Pencil className="h-3.5 w-3.5" /></Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -297,23 +297,23 @@ const RESOURCE_CONFIG: Record<ResourceType, {
|
||||
const subjects = rb.subjects || [];
|
||||
const subjStr = subjects.length > 3 ? subjects.slice(0, 3).join(', ') + ` +${subjects.length - 3}` : subjects.join(', ');
|
||||
return (
|
||||
<TableRow key={rb.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||
<TableCell className="font-medium text-slate-200">{rb.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{rb.namespace}</Badge></TableCell>
|
||||
<TableRow key={rb.name} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/30">
|
||||
<TableCell className="font-medium text-[var(--dg-text-primary)]">{rb.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">{rb.namespace}</Badge></TableCell>
|
||||
<TableCell>
|
||||
<Badge className="bg-purple-500/20 text-purple-400 border-purple-500/30">
|
||||
<Badge className="bg-[var(--dg-purple)]/20 text-[var(--dg-purple)] border-[var(--dg-purple)]/30">
|
||||
{rb.role_ref_kind || '-'}/{rb.role_ref_name || '-'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-slate-500 max-w-[200px] truncate" title={subjects.join(', ')}>
|
||||
<TableCell className="text-xs text-[var(--dg-text-muted)] max-w-[200px] truncate" title={subjects.join(', ')}>
|
||||
{subjStr || '-'}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-slate-500">{rb.age || '-'}</TableCell>
|
||||
<TableCell className="text-xs text-[var(--dg-text-muted)]">{rb.age || '-'}</TableCell>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-blue-400" title="查看YAML"
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-text-secondary)] hover:text-[var(--dg-blue)]" title="查看YAML"
|
||||
onClick={() => opts.onYaml?.(rb.name)}><FileCode className="h-3.5 w-3.5" /></Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-emerald-400 hover:text-emerald-300" title="编辑"
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-green)] hover:text-[var(--dg-green)]" title="编辑"
|
||||
onClick={() => opts.onEdit?.(rb.name)}><Pencil className="h-3.5 w-3.5" /></Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -461,7 +461,7 @@ export default function NamespaceResourcePage() {
|
||||
}
|
||||
};
|
||||
|
||||
if (!config) return <div className="text-slate-400 p-8 text-center">未知资源类型: {type}</div>;
|
||||
if (!config) return <div className="text-[var(--dg-text-secondary)] p-8 text-center">未知资源类型: {type}</div>;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -495,50 +495,50 @@ export default function NamespaceResourcePage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 text-sm text-slate-500">
|
||||
<Link to="/kubernetes" className="hover:text-slate-300 transition-colors">集群</Link>
|
||||
{clusterCode && <><ChevronRight className="h-3 w-3" /><Link to={`/kubernetes/${clusterCode}`} className="hover:text-slate-300 transition-colors">{clusterCode}</Link></>}
|
||||
{namespace && <><ChevronRight className="h-3 w-3" /><Link to={`/kubernetes/${clusterCode}/namespace/${namespace}`} className="hover:text-slate-300 transition-colors">{namespace}</Link></>}
|
||||
<ChevronRight className="h-3 w-3" /><span className="text-slate-300">{config.title}</span>
|
||||
<div className="flex items-center gap-2 text-sm text-[var(--dg-text-muted)]">
|
||||
<Link to="/kubernetes" className="hover:text-[var(--dg-text-secondary)] transition-colors">集群</Link>
|
||||
{clusterCode && <><ChevronRight className="h-3 w-3" /><Link to={`/kubernetes/${clusterCode}`} className="hover:text-[var(--dg-text-secondary)] transition-colors">{clusterCode}</Link></>}
|
||||
{namespace && <><ChevronRight className="h-3 w-3" /><Link to={`/kubernetes/${clusterCode}/namespace/${namespace}`} className="hover:text-[var(--dg-text-secondary)] transition-colors">{namespace}</Link></>}
|
||||
<ChevronRight className="h-3 w-3" /><span className="text-[var(--dg-text-secondary)]">{config.title}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-100">{config.title}</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">{config.subtitle}</p>
|
||||
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)]">{config.title}</h1>
|
||||
<p className="text-sm text-[var(--dg-text-muted)] mt-1">{config.subtitle}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={clusterCode || ''} onValueChange={(v) => navigate(`/kubernetes/${v}/namespace/${namespace || 'default'}/${type}`)}>
|
||||
<SelectTrigger className="w-48 h-9 bg-slate-800/50 border-slate-700 text-slate-200"><SelectValue placeholder="选择集群" /></SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">{clusters.map((c) => <SelectItem key={c.id} value={c.code || String(c.id)}>{c.name}</SelectItem>)}</SelectContent>
|
||||
<SelectTrigger className="w-48 h-9 bg-[var(--dg-bg-tertiary)]/50 border-[var(--dg-border)] text-[var(--dg-text-primary)]"><SelectValue placeholder="选择集群" /></SelectTrigger>
|
||||
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">{clusters.map((c) => <SelectItem key={c.id} value={c.code || String(c.id)}>{c.name}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
<Select value={namespace || ''} onValueChange={(v) => navigate(`/kubernetes/${clusterCode}/namespace/${v}/${type}`)}>
|
||||
<SelectTrigger className="w-40 h-9 bg-slate-800/50 border-slate-700 text-slate-200"><SelectValue placeholder="命名空间" /></SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700 max-h-[300px]">{namespaces.map((ns) => <SelectItem key={ns} value={ns}>{ns}</SelectItem>)}</SelectContent>
|
||||
<SelectTrigger className="w-40 h-9 bg-[var(--dg-bg-tertiary)]/50 border-[var(--dg-border)] text-[var(--dg-text-primary)]"><SelectValue placeholder="命名空间" /></SelectTrigger>
|
||||
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] max-h-[300px]">{namespaces.map((ns) => <SelectItem key={ns} value={ns}>{ns}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="icon" className="h-9 w-9 border-slate-700 text-slate-400" onClick={loadData} disabled={loading}>
|
||||
<Button variant="outline" size="icon" className="h-9 w-9 border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={loadData} disabled={loading}>
|
||||
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
|
||||
</Button>
|
||||
{BULK_EXPORT_TYPES.includes(type) && (
|
||||
<>
|
||||
<Button variant="outline" size="sm" className="h-9 border-slate-700 text-slate-400 text-xs" onClick={handleExportCSV} title="导出CSV">
|
||||
<Button variant="outline" size="sm" className="h-9 border-[var(--dg-border)] text-[var(--dg-text-secondary)] text-xs" onClick={handleExportCSV} title="导出CSV">
|
||||
<Download className="h-3 w-3 mr-1" />CSV
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="h-9 border-slate-700 text-slate-400 text-xs" onClick={handleExportJSON} title="导出JSON">
|
||||
<Button variant="outline" size="sm" className="h-9 border-[var(--dg-border)] text-[var(--dg-text-secondary)] text-xs" onClick={handleExportJSON} title="导出JSON">
|
||||
<Download className="h-3 w-3 mr-1" />JSON
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{EXPORT_YAML_TYPES.includes(type) && (
|
||||
<Button variant="outline" size="sm" className="h-9 border-slate-700 text-slate-400 text-xs" onClick={handleExportYaml} title="导出命名空间 YAML(tar.gz)">
|
||||
<Button variant="outline" size="sm" className="h-9 border-[var(--dg-border)] text-[var(--dg-text-secondary)] text-xs" onClick={handleExportYaml} title="导出命名空间 YAML(tar.gz)">
|
||||
<Download className="h-3 w-3 mr-1" />导出YAML
|
||||
</Button>
|
||||
)}
|
||||
{(VISUAL_RESOURCE_TYPES.includes(type) || DEPLOYMENT_LIST_TYPES.includes(type)) && (
|
||||
<>
|
||||
<Button variant="outline" size="sm" className="h-9 border-slate-700 text-slate-400 text-xs" onClick={() => setShowYamlImport(true)}>
|
||||
<Button variant="outline" size="sm" className="h-9 border-[var(--dg-border)] text-[var(--dg-text-secondary)] text-xs" onClick={() => setShowYamlImport(true)}>
|
||||
<Upload className="h-3 w-3 mr-1" />导入
|
||||
</Button>
|
||||
<Button size="sm" className="h-9 text-xs bg-blue-600 hover:bg-blue-700" onClick={handleCreate}>
|
||||
<Button size="sm" className="h-9 text-xs bg-[var(--dg-blue)] hover:opacity-90" onClick={handleCreate}>
|
||||
<Plus className="h-3 w-3 mr-1" />新建
|
||||
</Button>
|
||||
</>
|
||||
@@ -547,19 +547,19 @@ export default function NamespaceResourcePage() {
|
||||
</div>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<div className="relative w-56 flex-1 min-w-[200px] max-w-xs">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
|
||||
<Input placeholder="搜索..." value={search} onChange={(e) => setSearch(e.target.value)} className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||
<Input placeholder="搜索..." value={search} onChange={(e) => setSearch(e.target.value)} className="pl-9 h-9 bg-[var(--dg-bg-tertiary)]/50 border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
</div>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader><TableRow className="border-slate-800 hover:bg-transparent">{config.columns.map((col) => <TableHead key={col} className="text-slate-400">{col}</TableHead>)}</TableRow></TableHeader>
|
||||
<TableHeader><TableRow className="border-[var(--dg-border)] hover:bg-transparent">{config.columns.map((col) => <TableHead key={col} className="text-[var(--dg-text-secondary)]">{col}</TableHead>)}</TableRow></TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow><TableCell colSpan={config.columns.length} className="text-center text-slate-500 py-12"><Loader2 className="h-5 w-5 animate-spin mx-auto mb-2" />加载中...</TableCell></TableRow>
|
||||
<TableRow><TableCell colSpan={config.columns.length} className="text-center text-[var(--dg-text-muted)] py-12"><Loader2 className="h-5 w-5 animate-spin mx-auto mb-2" />加载中...</TableCell></TableRow>
|
||||
) : filtered.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={config.columns.length} className="text-center text-slate-500 py-12"><config.icon className="h-8 w-8 mx-auto mb-2 opacity-50" />暂无 {config.title}</TableCell></TableRow>
|
||||
<TableRow><TableCell colSpan={config.columns.length} className="text-center text-[var(--dg-text-muted)] py-12"><config.icon className="h-8 w-8 mx-auto mb-2 opacity-50" />暂无 {config.title}</TableCell></TableRow>
|
||||
) : (
|
||||
filtered.map((item) => config.renderRow(item, { clusterCode: clusterCode || '', namespace: namespace || '', onDetail: handleDetail, onYaml: handleYaml, onEdit: handleEdit, onTerminal: ( p: any) => setTerminalPod(p), onFileBrowser: ( p: any) => setFileBrowserPod(p), onDeletePod: handleDeletePod, onPodDetail: ( p: any) => setPodDetail(p) }))
|
||||
)}
|
||||
@@ -606,7 +606,7 @@ export default function NamespaceResourcePage() {
|
||||
const labels = p.labels || p.metadata?.labels || {};
|
||||
const annotations = p.annotations || p.metadata?.annotations || {};
|
||||
const created = p.creationTimestamp || p.created_at || p.create_time;
|
||||
const statusMap: Record<string, string> = { Running: 'bg-emerald-500/20 text-emerald-400', Pending: 'bg-yellow-500/20 text-yellow-400', Failed: 'bg-red-500/20 text-red-400' };
|
||||
const statusMap: Record<string, string> = { Running: 'bg-[var(--dg-green)]/20 text-[var(--dg-green)]', Pending: 'bg-[var(--dg-orange)]/20 text-[var(--dg-orange)]', Failed: 'bg-[var(--dg-red)]/20 text-[var(--dg-red)]' };
|
||||
const st = p.status || 'Running';
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-x-6 gap-y-2.5">
|
||||
@@ -620,7 +620,7 @@ export default function NamespaceResourcePage() {
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs text-[var(--dg-text-muted)]">状态</span>
|
||||
<div><Badge className={statusMap[st] || 'bg-slate-500/20 text-slate-400'}>{st}</Badge></div>
|
||||
<div><Badge className={statusMap[st] || 'bg-[var(--dg-text-muted)]/20 text-[var(--dg-text-secondary)]'}>{st}</Badge></div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs text-[var(--dg-text-muted)]">创建时间</span>
|
||||
@@ -689,7 +689,7 @@ export default function NamespaceResourcePage() {
|
||||
<TableRow key={idx} className="border-[var(--dg-border)]">
|
||||
<TableCell className="text-xs text-[var(--dg-text-primary)] font-medium">{type}</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={isTrue ? 'bg-emerald-500/15 text-emerald-400 border-emerald-500/25' : isFalse ? 'bg-red-500/15 text-red-400 border-red-500/25' : 'bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-muted)]'}>{st}</Badge>
|
||||
<Badge className={isTrue ? 'bg-[var(--dg-green)]/15 text-[var(--dg-green)] border-[var(--dg-green)]/25' : isFalse ? 'bg-[var(--dg-red)]/15 text-[var(--dg-red)] border-[var(--dg-red)]/25' : 'bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-muted)]'}>{st}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-[var(--dg-text-muted)] whitespace-nowrap">{ts ? new Date(ts).toLocaleString('zh-CN') : '-'}</TableCell>
|
||||
<TableCell className="text-xs text-[var(--dg-text-primary)]">{reason}</TableCell>
|
||||
|
||||
@@ -279,6 +279,8 @@ export function PodExecTerminal({
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
// 禁用 Radix Dialog 默认的 ESC 关闭:Pod exec 终端会用到 ESC,关闭只能点头部 X 按钮 // Logan.Li
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
className={cn(
|
||||
'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] p-0 gap-0',
|
||||
'!duration-0 !transition-none',
|
||||
|
||||
@@ -27,11 +27,11 @@ type ViewMode = 'view' | 'yaml';
|
||||
|
||||
function StatCard({ label, value, sub, color }: { label: string; value: React.ReactNode; sub?: string; color?: string }) {
|
||||
return (
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-3">
|
||||
<p className="text-xs text-slate-500">{label}</p>
|
||||
<p className="text-base font-bold mt-0.5" style={{ color: color || '#e2e8f0' }}>{value}</p>
|
||||
{sub && <p className="text-[10px] text-slate-600 mt-0.5">{sub}</p>}
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">{label}</p>
|
||||
<p className="text-base font-bold mt-0.5" style={{ color: color || 'var(--dg-text-primary)' }}>{value}</p>
|
||||
{sub && <p className="text-[10px] text-[var(--dg-text-muted)] mt-0.5">{sub}</p>}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -40,11 +40,11 @@ function StatCard({ label, value, sub, color }: { label: string; value: React.Re
|
||||
// Helper: render key-value pairs as badges
|
||||
function LabelBadges({ labels }: { labels: Record<string, string> | undefined }) {
|
||||
const entries = Object.entries(labels || {});
|
||||
if (entries.length === 0) return <span className="text-xs text-slate-600">无</span>;
|
||||
if (entries.length === 0) return <span className="text-xs text-[var(--dg-text-muted)]">无</span>;
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{entries.map(([k, v]) => (
|
||||
<Badge key={k} variant="outline" className="border-slate-700 text-slate-400 text-[10px] font-mono">
|
||||
<Badge key={k} variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] text-[10px] font-mono">
|
||||
{k}={v}
|
||||
</Badge>
|
||||
))}
|
||||
@@ -192,46 +192,46 @@ export default function WorkloadViewPage() {
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-slate-200" onClick={() => navigate(-1)}>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]" onClick={() => navigate(-1)}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<Boxes className="h-5 w-5 text-blue-400" />
|
||||
<Boxes className="h-5 w-5 text-[var(--dg-blue)]" />
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-lg font-bold text-slate-100">{name}</h1>
|
||||
<Badge variant="outline" className="border-slate-700 text-slate-400">{kind}</Badge>
|
||||
<Badge variant="outline" className="border-slate-700 text-slate-400">{namespace}</Badge>
|
||||
<h1 className="text-lg font-bold text-[var(--dg-text-primary)]">{name}</h1>
|
||||
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">{kind}</Badge>
|
||||
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">{namespace}</Badge>
|
||||
{isHealthy
|
||||
? <Badge className="bg-emerald-500/20 text-emerald-400 border-emerald-500/30">健康</Badge>
|
||||
: <Badge className="bg-amber-500/20 text-amber-400 border-amber-500/30">更新中</Badge>}
|
||||
? <Badge className="bg-[var(--dg-green)]/20 text-[var(--dg-green)] border-[var(--dg-green)]/30">健康</Badge>
|
||||
: <Badge className="bg-[var(--dg-orange)]/20 text-[var(--dg-orange)] border-[var(--dg-orange)]/30">更新中</Badge>}
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-0.5">集群: {clusterCode}</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mt-0.5">集群: {clusterCode}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Mode tabs */}
|
||||
<div className="flex items-center gap-1 bg-slate-900 border border-slate-800 rounded-lg p-0.5">
|
||||
<div className="flex items-center gap-1 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-lg p-0.5">
|
||||
<button
|
||||
className={`px-3 py-1 text-xs rounded transition-colors ${mode === 'view' ? 'bg-blue-500/20 text-blue-400' : 'text-slate-400 hover:text-slate-200'}`}
|
||||
className={`px-3 py-1 text-xs rounded transition-colors ${mode === 'view' ? 'bg-[var(--dg-blue)]/20 text-[var(--dg-blue)]' : 'text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]'}`}
|
||||
onClick={() => handleModeChange('view')}
|
||||
>查看</button>
|
||||
<button
|
||||
className={`px-3 py-1 text-xs rounded transition-colors ${mode === 'yaml' ? 'bg-blue-500/20 text-blue-400' : 'text-slate-400 hover:text-slate-200'}`}
|
||||
className={`px-3 py-1 text-xs rounded transition-colors ${mode === 'yaml' ? 'bg-[var(--dg-blue)]/20 text-[var(--dg-blue)]' : 'text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]'}`}
|
||||
onClick={() => handleModeChange('yaml')}
|
||||
>YAML</button>
|
||||
</div>
|
||||
{/* Actions */}
|
||||
{isSts && (
|
||||
<Button variant="outline" size="sm" className="h-8 border-slate-700 text-slate-300 hover:text-slate-100"
|
||||
<Button variant="outline" size="sm" className="h-8 border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]"
|
||||
onClick={() => { setScaleValue(replicas); setScaleModal(true); }}>
|
||||
<Boxes className="h-3.5 w-3.5 mr-1" />伸缩
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" size="sm" className="h-8 border-slate-700 text-slate-300 hover:text-slate-100"
|
||||
<Button variant="outline" size="sm" className="h-8 border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]"
|
||||
disabled={actionLoading} onClick={handleRestart}>
|
||||
<RotateCcw className="h-3.5 w-3.5 mr-1" />{actionLoading ? '重启中...' : '重启'}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="h-8 border-slate-700 text-slate-300 hover:text-slate-100"
|
||||
<Button variant="outline" size="sm" className="h-8 border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]"
|
||||
onClick={() => setYamlModal(true)}>
|
||||
<FileCode className="h-3.5 w-3.5 mr-1" />YAML
|
||||
</Button>
|
||||
@@ -239,9 +239,9 @@ export default function WorkloadViewPage() {
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center text-slate-500 py-20">加载中...</div>
|
||||
<div className="text-center text-[var(--dg-text-muted)] py-20">加载中...</div>
|
||||
) : mode === 'yaml' ? (
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-4">
|
||||
<YamlViewer
|
||||
clusterCode={clusterCode}
|
||||
@@ -263,45 +263,45 @@ export default function WorkloadViewPage() {
|
||||
<StatCard label="副本就绪" value={`${readyReplicas}/${replicas}`} color={isHealthy ? STATUS_COLORS.green : STATUS_COLORS.orange} sub={isHealthy ? '全部就绪' : '部分未就绪'} />
|
||||
<StatCard label="当前副本" value={currentReplicas} color={STATUS_COLORS.blue} />
|
||||
<StatCard label="已更新副本" value={updatedReplicas} color={STATUS_COLORS.green} />
|
||||
<StatCard label="服务名" value={spec.serviceName || '-'} color="#94a3b8" />
|
||||
<StatCard label="Pod 数量" value={pods.length} color="#94a3b8" />
|
||||
<StatCard label="创建时间" value={<span className="text-xs">{formatK8sTime(manifest?.metadata?.creationTimestamp)}</span>} color="#94a3b8" />
|
||||
<StatCard label="服务名" value={spec.serviceName || '-'} color="var(--dg-text-secondary)" />
|
||||
<StatCard label="Pod 数量" value={pods.length} color="var(--dg-text-secondary)" />
|
||||
<StatCard label="创建时间" value={<span className="text-xs">{formatK8sTime(manifest?.metadata?.creationTimestamp)}</span>} color="var(--dg-text-secondary)" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<StatCard label="期望调度" value={desiredScheduled} color={STATUS_COLORS.blue} />
|
||||
<StatCard label="当前调度" value={currentScheduled} color="#94a3b8" />
|
||||
<StatCard label="当前调度" value={currentScheduled} color="var(--dg-text-secondary)" />
|
||||
<StatCard label="就绪数量" value={numberReady} color={isHealthy ? STATUS_COLORS.green : STATUS_COLORS.orange} />
|
||||
<StatCard label="可用数量" value={numberAvailable} color={STATUS_COLORS.green} />
|
||||
<StatCard label="已更新" value={numberUpdated} color="#94a3b8" />
|
||||
<StatCard label="创建时间" value={<span className="text-xs">{formatK8sTime(manifest?.metadata?.creationTimestamp)}</span>} color="#94a3b8" />
|
||||
<StatCard label="已更新" value={numberUpdated} color="var(--dg-text-secondary)" />
|
||||
<StatCard label="创建时间" value={<span className="text-xs">{formatK8sTime(manifest?.metadata?.creationTimestamp)}</span>} color="var(--dg-text-secondary)" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Update Strategy + Selector */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-slate-200 flex items-center gap-2">
|
||||
<Settings className="h-4 w-4 text-amber-400" />更新策略
|
||||
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<Settings className="h-4 w-4 text-[var(--dg-orange)]" />更新策略
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 space-y-2 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500">策略类型</span>
|
||||
<Badge variant="outline" className="border-slate-700 text-slate-300">{strategyType}</Badge>
|
||||
<span className="text-[var(--dg-text-muted)]">策略类型</span>
|
||||
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">{strategyType}</Badge>
|
||||
</div>
|
||||
{strategyType === 'RollingUpdate' && isSts && updateStrategy.rollingUpdate && (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500">分区 (partition)</span>
|
||||
<span className="text-slate-300 font-mono">{updateStrategy.rollingUpdate.partition ?? 0}</span>
|
||||
<span className="text-[var(--dg-text-muted)]">分区 (partition)</span>
|
||||
<span className="text-[var(--dg-text-secondary)] font-mono">{updateStrategy.rollingUpdate.partition ?? 0}</span>
|
||||
</div>
|
||||
{updateStrategy.rollingUpdate.maxUnavailable && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500">最大不可用</span>
|
||||
<span className="text-slate-300 font-mono">{typeof updateStrategy.rollingUpdate.maxUnavailable === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxUnavailable) : updateStrategy.rollingUpdate.maxUnavailable}</span>
|
||||
<span className="text-[var(--dg-text-muted)]">最大不可用</span>
|
||||
<span className="text-[var(--dg-text-secondary)] font-mono">{typeof updateStrategy.rollingUpdate.maxUnavailable === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxUnavailable) : updateStrategy.rollingUpdate.maxUnavailable}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -310,52 +310,52 @@ export default function WorkloadViewPage() {
|
||||
<>
|
||||
{updateStrategy.rollingUpdate.maxUnavailable && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500">最大不可用</span>
|
||||
<span className="text-slate-300 font-mono">{typeof updateStrategy.rollingUpdate.maxUnavailable === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxUnavailable) : updateStrategy.rollingUpdate.maxUnavailable}</span>
|
||||
<span className="text-[var(--dg-text-muted)]">最大不可用</span>
|
||||
<span className="text-[var(--dg-text-secondary)] font-mono">{typeof updateStrategy.rollingUpdate.maxUnavailable === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxUnavailable) : updateStrategy.rollingUpdate.maxUnavailable}</span>
|
||||
</div>
|
||||
)}
|
||||
{updateStrategy.rollingUpdate.maxSurge && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500">最大 surge</span>
|
||||
<span className="text-slate-300 font-mono">{typeof updateStrategy.rollingUpdate.maxSurge === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxSurge) : updateStrategy.rollingUpdate.maxSurge}</span>
|
||||
<span className="text-[var(--dg-text-muted)]">最大 surge</span>
|
||||
<span className="text-[var(--dg-text-secondary)] font-mono">{typeof updateStrategy.rollingUpdate.maxSurge === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxSurge) : updateStrategy.rollingUpdate.maxSurge}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isSts && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500">Pod 管理策略</span>
|
||||
<span className="text-slate-300 font-mono">{spec.podManagementPolicy || 'OrderedReady'}</span>
|
||||
<span className="text-[var(--dg-text-muted)]">Pod 管理策略</span>
|
||||
<span className="text-[var(--dg-text-secondary)] font-mono">{spec.podManagementPolicy || 'OrderedReady'}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500">修订历史限制</span>
|
||||
<span className="text-slate-300 font-mono">{spec.revisionHistoryLimit ?? 10}</span>
|
||||
<span className="text-[var(--dg-text-muted)]">修订历史限制</span>
|
||||
<span className="text-[var(--dg-text-secondary)] font-mono">{spec.revisionHistoryLimit ?? 10}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-slate-200 flex items-center gap-2">
|
||||
<Tag className="h-4 w-4 text-purple-400" />选择器与标签
|
||||
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<Tag className="h-4 w-4 text-[var(--dg-purple)]" />选择器与标签
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 space-y-3 text-xs">
|
||||
<div>
|
||||
<p className="text-slate-500 mb-1.5">Selector (matchLabels)</p>
|
||||
<p className="text-[var(--dg-text-muted)] mb-1.5">Selector (matchLabels)</p>
|
||||
<LabelBadges labels={selectorLabels} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-slate-500 mb-1.5">Labels</p>
|
||||
<p className="text-[var(--dg-text-muted)] mb-1.5">Labels</p>
|
||||
<LabelBadges labels={labels} />
|
||||
</div>
|
||||
{Object.keys(annotations).length > 0 && (
|
||||
<div>
|
||||
<p className="text-slate-500 mb-1.5">Annotations</p>
|
||||
<p className="text-[var(--dg-text-muted)] mb-1.5">Annotations</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{Object.entries(annotations).slice(0, 8).map(([k, _v]) => (
|
||||
<Badge key={k} variant="outline" className="border-slate-700 text-slate-500 text-[10px] font-mono max-w-[300px] truncate">
|
||||
<Badge key={k} variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-muted)] text-[10px] font-mono max-w-[300px] truncate">
|
||||
{k}
|
||||
</Badge>
|
||||
))}
|
||||
@@ -368,20 +368,20 @@ export default function WorkloadViewPage() {
|
||||
|
||||
{/* VolumeClaimTemplates (StatefulSet only) */}
|
||||
{isSts && vcts.length > 0 && (
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-slate-200 flex items-center gap-2">
|
||||
<Database className="h-4 w-4 text-cyan-400" />存储卷声明模板 (VolumeClaimTemplates)
|
||||
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<Database className="h-4 w-4 text-[var(--dg-cyan)]" />存储卷声明模板 (VolumeClaimTemplates)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-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">大小</TableHead>
|
||||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">名称</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">访问模式</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">存储类</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">大小</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -390,11 +390,11 @@ export default function WorkloadViewPage() {
|
||||
const storageClass = vct.spec?.storageClassName || vct.spec?.storageClassName || '-';
|
||||
const size = vct.spec?.resources?.requests?.storage || '-';
|
||||
return (
|
||||
<TableRow key={idx} className="border-slate-800">
|
||||
<TableCell className="font-medium text-slate-200">{vct.metadata?.name || '-'}</TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{accessModes || '-'}</TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{storageClass}</TableCell>
|
||||
<TableCell className="text-sm text-slate-300 font-mono">{size}</TableCell>
|
||||
<TableRow key={idx} className="border-[var(--dg-border)]">
|
||||
<TableCell className="font-medium text-[var(--dg-text-primary)]">{vct.metadata?.name || '-'}</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{accessModes || '-'}</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{storageClass}</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)] font-mono">{size}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
@@ -405,16 +405,16 @@ export default function WorkloadViewPage() {
|
||||
)}
|
||||
|
||||
{/* Container template */}
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-slate-200 flex items-center gap-2">
|
||||
<Container className="h-4 w-4 text-blue-400" />容器模板(Pod Template)
|
||||
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<Container className="h-4 w-4 text-[var(--dg-blue)]" />容器模板(Pod Template)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
{initContainers.length > 0 && (
|
||||
<div className="mb-3">
|
||||
<p className="text-[11px] text-slate-500 mb-2 uppercase tracking-wide">Init Containers</p>
|
||||
<p className="text-[11px] text-[var(--dg-text-muted)] mb-2 uppercase tracking-wide">Init Containers</p>
|
||||
<div className="space-y-2">
|
||||
{initContainers.map((c: JsonData, idx: number) => (
|
||||
<ContainerInfo key={`init-${idx}`} c={c} />
|
||||
@@ -423,10 +423,10 @@ export default function WorkloadViewPage() {
|
||||
</div>
|
||||
)}
|
||||
{containers.length === 0 ? (
|
||||
<p className="text-xs text-slate-500 py-3">暂无容器数据,请查看 YAML</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] py-3">暂无容器数据,请查看 YAML</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<p className="text-[11px] text-slate-500 mb-1 uppercase tracking-wide">Containers</p>
|
||||
<p className="text-[11px] text-[var(--dg-text-muted)] mb-1 uppercase tracking-wide">Containers</p>
|
||||
{containers.map((c: JsonData, idx: number) => (
|
||||
<ContainerInfo key={idx} c={c} />
|
||||
))}
|
||||
@@ -436,45 +436,45 @@ export default function WorkloadViewPage() {
|
||||
</Card>
|
||||
|
||||
{/* Pod list */}
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-slate-200 flex items-center gap-2">
|
||||
<Activity className="h-4 w-4 text-emerald-400" />Pod 实例 ({pods.length})
|
||||
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<Activity className="h-4 w-4 text-[var(--dg-green)]" />Pod 实例 ({pods.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-slate-800 hover:bg-transparent">
|
||||
<TableHead className="text-slate-400">Pod 名称</TableHead>
|
||||
<TableHead className="text-slate-400">状态</TableHead>
|
||||
<TableHead className="text-slate-400">所在节点</TableHead>
|
||||
<TableHead className="text-slate-400">Pod IP</TableHead>
|
||||
<TableHead className="text-slate-400">重启</TableHead>
|
||||
<TableHead className="text-slate-400">操作</TableHead>
|
||||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">Pod 名称</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">状态</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">所在节点</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">Pod IP</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">重启</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{pods.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={6} className="text-center text-slate-500 py-8">暂无 Pod</TableCell></TableRow>
|
||||
<TableRow><TableCell colSpan={6} className="text-center text-[var(--dg-text-muted)] py-8">暂无 Pod</TableCell></TableRow>
|
||||
) : pods.map((pod: JsonData) => (
|
||||
<TableRow key={pod.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||
<TableCell className="font-medium text-slate-200">{pod.name}</TableCell>
|
||||
<TableRow key={pod.name} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/30">
|
||||
<TableCell className="font-medium text-[var(--dg-text-primary)]">{pod.name}</TableCell>
|
||||
<TableCell>
|
||||
<Badge style={{ background: `${statusColor(pod.status)}22`, color: statusColor(pod.status), border: `1px solid ${statusColor(pod.status)}44` }}>
|
||||
{pod.status || 'Unknown'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{pod.node_name || pod.node || '-'}</TableCell>
|
||||
<TableCell className="text-sm text-slate-400 font-mono">{pod.ip || pod.pod_ip || '-'}</TableCell>
|
||||
<TableCell className={(pod.restart_count || 0) > 0 ? 'text-yellow-400' : 'text-slate-500'}>{pod.restart_count || 0}</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{pod.node_name || pod.node || '-'}</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)] font-mono">{pod.ip || pod.pod_ip || '-'}</TableCell>
|
||||
<TableCell className={(pod.restart_count || 0) > 0 ? 'text-[var(--dg-orange)]' : 'text-[var(--dg-text-muted)]'}>{pod.restart_count || 0}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-emerald-400 hover:text-emerald-300 hover:bg-emerald-500/10"
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-[var(--dg-green)] hover:text-[var(--dg-green)] hover:bg-[var(--dg-green)]/10"
|
||||
onClick={() => setTerminalPod(pod)}>
|
||||
<Terminal className="h-3.5 w-3.5 mr-1" />终端
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-amber-400 hover:text-amber-300 hover:bg-amber-500/10"
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-[var(--dg-orange)] hover:text-[var(--dg-orange)] hover:bg-[var(--dg-orange)]/10"
|
||||
onClick={() => setFileBrowserPod(pod)}>
|
||||
<HardDrive className="h-3.5 w-3.5 mr-1" />文件浏览器
|
||||
</Button>
|
||||
@@ -492,20 +492,20 @@ export default function WorkloadViewPage() {
|
||||
{/* Scale modal */}
|
||||
{scaleModal && (
|
||||
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center" onClick={() => setScaleModal(false)}>
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-lg p-6 w-80" onClick={e => e.stopPropagation()}>
|
||||
<h3 className="text-sm font-semibold text-slate-200 mb-4">伸缩 StatefulSet</h3>
|
||||
<div className="bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-lg p-6 w-80" onClick={e => e.stopPropagation()}>
|
||||
<h3 className="text-sm font-semibold text-[var(--dg-text-primary)] mb-4">伸缩 StatefulSet</h3>
|
||||
<div className="flex items-center justify-center gap-4 mb-4">
|
||||
<Button variant="outline" size="icon" className="h-9 w-9" onClick={() => setScaleValue(Math.max(0, scaleValue - 1))}>
|
||||
<Minus className="h-4 w-4" />
|
||||
</Button>
|
||||
<span className="text-2xl font-bold text-slate-100 w-12 text-center">{scaleValue}</span>
|
||||
<span className="text-2xl font-bold text-[var(--dg-text-primary)] w-12 text-center">{scaleValue}</span>
|
||||
<Button variant="outline" size="icon" className="h-9 w-9" onClick={() => setScaleValue(scaleValue + 1)}>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" className="flex-1 border-slate-700 text-slate-400" onClick={() => setScaleModal(false)}>取消</Button>
|
||||
<Button size="sm" className="flex-1 bg-blue-600 hover:bg-blue-500" disabled={actionLoading} onClick={handleScale}>
|
||||
<Button variant="outline" size="sm" className="flex-1 border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={() => setScaleModal(false)}>取消</Button>
|
||||
<Button size="sm" className="flex-1 bg-[var(--dg-blue)] hover:opacity-90" disabled={actionLoading} onClick={handleScale}>
|
||||
{actionLoading ? '执行中...' : '确认'}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -554,23 +554,23 @@ export default function WorkloadViewPage() {
|
||||
// Container info sub-component
|
||||
function ContainerInfo({ c }: { c: JsonData}) {
|
||||
return (
|
||||
<div className="border border-slate-800 rounded-lg p-3 bg-slate-950/30">
|
||||
<div className="border border-[var(--dg-border)] rounded-lg p-3 bg-[var(--dg-bg-primary)]/30">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Container className="h-4 w-4 text-blue-400" />
|
||||
<span className="text-sm font-semibold text-slate-200">{c.name}</span>
|
||||
<Container className="h-4 w-4 text-[var(--dg-blue)]" />
|
||||
<span className="text-sm font-semibold text-[var(--dg-text-primary)]">{c.name}</span>
|
||||
</div>
|
||||
<code className="text-[11px] text-slate-400 font-mono truncate max-w-[400px]">{c.image}</code>
|
||||
<code className="text-[11px] text-[var(--dg-text-secondary)] font-mono truncate max-w-[400px]">{c.image}</code>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 text-xs">
|
||||
<div className="text-slate-400">
|
||||
CPU: <span className="text-slate-200 font-mono">{c.resources?.requests?.cpu || '-'}/{c.resources?.limits?.cpu || '-'}</span>
|
||||
<div className="text-[var(--dg-text-secondary)]">
|
||||
CPU: <span className="text-[var(--dg-text-primary)] font-mono">{c.resources?.requests?.cpu || '-'}/{c.resources?.limits?.cpu || '-'}</span>
|
||||
</div>
|
||||
<div className="text-slate-400">
|
||||
内存: <span className="text-slate-200 font-mono">{c.resources?.requests?.memory || '-'}/{c.resources?.limits?.memory || '-'}</span>
|
||||
<div className="text-[var(--dg-text-secondary)]">
|
||||
内存: <span className="text-[var(--dg-text-primary)] font-mono">{c.resources?.requests?.memory || '-'}/{c.resources?.limits?.memory || '-'}</span>
|
||||
</div>
|
||||
<div className="text-slate-400">
|
||||
端口: <span className="text-slate-200 font-mono">
|
||||
<div className="text-[var(--dg-text-secondary)]">
|
||||
端口: <span className="text-[var(--dg-text-primary)] font-mono">
|
||||
{(c.ports || []).map((p: JsonData) => p.containerPort || p.port).join(', ') || '-'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user