feat: Agent CLI 与终端审计回放全面优化

- Agent CLI: 多轮对话、SSH 长连接、流式推理、Evolver 按用户隔离、历史侧栏
- 终端审计: 回放播放器、命令书签、筛选分页导出、命令输出统计、录制落盘修复
- 修复 session_commands 表迁移、Kill 竞态导致录制丢失、无录制会话回放提示
- 审计历史页只读(移除删除),移除回放弹框录制搜索
This commit is contained in:
Logan committed 2026-07-01 12:13:53 +08:00
1 parent 9a3dd304f7
commit 1acb0c49af
81 files changed
+3916 -1655

No files matched your search

@@ -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>