- Agent CLI: 多轮对话、SSH 长连接、流式推理、Evolver 按用户隔离、历史侧栏 - 终端审计: 回放播放器、命令书签、筛选分页导出、命令输出统计、录制落盘修复 - 修复 session_commands 表迁移、Kill 竞态导致录制丢失、无录制会话回放提示 - 审计历史页只读(移除删除),移除回放弹框录制搜索
715 lines
44 KiB
TypeScript
715 lines
44 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||
import { useParams, useNavigate, useLocation, Link } from 'react-router-dom';
|
||
import { Card, CardContent } from '@/components/ui/card';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Input } from '@/components/ui/input';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import {
|
||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||
} from '@/components/ui/select';
|
||
import {
|
||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||
} from '@/components/ui/table';
|
||
import {
|
||
RefreshCw, Search, Loader2, Boxes, Container,
|
||
Network, FileText, Shield, Activity, Clock, ChevronRight,
|
||
Download, FileCode, Pencil, Terminal, HardDrive, Plus, Upload, Trash2, Eye,
|
||
} from 'lucide-react';
|
||
import { YamlViewer } from './YamlViewer';
|
||
import { ResourceVisualEditor } from './ResourceVisualEditor';
|
||
import { YamlImportDialog } from './YamlImportDialog';
|
||
import { PodExecTerminal } from './PodExecTerminal';
|
||
import { PodFileBrowser } from './PodFileBrowser';
|
||
import { kubernetesApi } from '@/api';
|
||
import { useToast } from '@/hooks/use-toast';
|
||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||
|
||
type ResourceType =
|
||
| 'deployments' | 'pods' | 'services' | 'ingresses'
|
||
| 'configmaps' | 'secrets' | 'hpas' | 'cronjobs'
|
||
| 'pvcs' | 'statefulsets' | 'daemonsets'
|
||
| 'serviceaccounts' | 'roles' | 'rolebindings';
|
||
|
||
// 支持可视化编辑/新建的资源类型 // Logan.Li
|
||
const VISUAL_RESOURCE_TYPES: ResourceType[] = ['services', 'ingresses', 'configmaps', 'secrets', 'pvcs'];
|
||
/** Deployment 列表:新建 + YAML 导入,不做批量导出 // Logan.Li */
|
||
const DEPLOYMENT_LIST_TYPES: ResourceType[] = ['deployments'];
|
||
/** 支持命名空间 YAML 批量导出(对齐集群导出)// Logan.Li */
|
||
const EXPORT_YAML_TYPES: ResourceType[] = ['services', 'ingresses', 'configmaps', 'secrets'];
|
||
/** 列表页保留 CSV/JSON 导出的配置类资源 // Logan.Li */
|
||
const BULK_EXPORT_TYPES: ResourceType[] = ['services', 'ingresses', 'configmaps', 'secrets', 'pvcs'];
|
||
|
||
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-[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-[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>
|
||
);
|
||
|
||
const RESOURCE_CONFIG: Record<ResourceType, {
|
||
title: string;
|
||
subtitle: string;
|
||
icon: React.ComponentType<any>;
|
||
columns: string[];
|
||
renderRow: ( item: any, opts: { clusterCode: string; namespace: string; onDetail?: ( item: any) => void; onYaml?: (name: string) => void; onEdit?: (name: string) => void; onTerminal?: ( item: any) => void; onFileBrowser?: ( item: any) => void; onDeletePod?: ( item: any) => void; onPodDetail?: ( item: any) => void }) => React.ReactNode;
|
||
}> = {
|
||
deployments: {
|
||
title: 'Deployments', subtitle: 'Deployment 工作负载运行状态', icon: Boxes,
|
||
columns: ['名称', '命名空间', '副本', '镜像'],
|
||
renderRow: (d, opts) => (
|
||
<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-[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-[var(--dg-green)]' : 'text-[var(--dg-orange)]'}`}>{ready}/{total}</span>
|
||
</div>
|
||
);
|
||
})()}
|
||
</TableCell>
|
||
<TableCell className="text-xs text-[var(--dg-text-muted)] max-w-[200px] truncate">{d.image || '-'}</TableCell>
|
||
</TableRow>
|
||
),
|
||
},
|
||
pods: {
|
||
title: 'Pods', subtitle: 'Pod 实例运行状态', icon: Container,
|
||
columns: ['名称', '命名空间', '状态', '节点', 'CPU', '内存', '重启', '镜像', '操作'],
|
||
renderRow: (p, opts) => {
|
||
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-[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-[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-[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-[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-[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-[var(--dg-red)] hover:text-[var(--dg-red)]" title="删除"
|
||
onClick={() => opts.onDeletePod?.(p)}><Trash2 className="h-3.5 w-3.5" /></Button>
|
||
</div>
|
||
</TableCell>
|
||
</TableRow>
|
||
);
|
||
},
|
||
},
|
||
services: {
|
||
title: 'Services', subtitle: 'Service 网络服务', icon: Network,
|
||
columns: ['名称', '命名空间', '类型', 'Cluster IP', '端口', '操作'],
|
||
renderRow: (s, opts) => (
|
||
<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>
|
||
),
|
||
},
|
||
ingresses: {
|
||
title: 'Ingresses', subtitle: 'Ingress 路由规则', icon: Network,
|
||
columns: ['名称', '命名空间', 'Hosts', 'TLS', '操作'],
|
||
renderRow: (i, opts) => {
|
||
const hosts = i.hosts || [];
|
||
return (
|
||
<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>
|
||
);
|
||
},
|
||
},
|
||
configmaps: {
|
||
title: 'ConfigMaps', subtitle: '配置字典', icon: FileText,
|
||
columns: ['名称', '命名空间', '键数量', '操作'],
|
||
renderRow: (c, opts) => (
|
||
<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>
|
||
),
|
||
},
|
||
secrets: {
|
||
title: 'Secrets', subtitle: '密钥管理(仅名称与类型)', icon: Shield,
|
||
columns: ['名称', '命名空间', '类型', '操作'],
|
||
renderRow: (s, opts) => (
|
||
<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>
|
||
),
|
||
},
|
||
hpas: {
|
||
title: 'HPAs', subtitle: '水平自动伸缩', icon: Activity,
|
||
columns: ['名称', '命名空间', '目标', '最小/最大', 'CPU%', 'Mem%'],
|
||
renderRow: (h, _opts) => (
|
||
<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>
|
||
),
|
||
},
|
||
cronjobs: {
|
||
title: 'CronJobs', subtitle: '定时任务', icon: Clock,
|
||
columns: ['名称', '命名空间', '调度', '状态', '上次执行'],
|
||
renderRow: (cj) => (
|
||
<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>
|
||
),
|
||
},
|
||
pvcs: {
|
||
title: '存储卷声明', subtitle: 'PersistentVolumeClaim', icon: Boxes,
|
||
columns: ['名称', '命名空间', '状态', '大小', 'StorageClass', '操作'],
|
||
renderRow: (p, opts) => (
|
||
<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>
|
||
),
|
||
},
|
||
statefulsets: {
|
||
title: 'StatefulSets', subtitle: '有状态应用', icon: Boxes,
|
||
columns: ['名称', '命名空间', '副本', '就绪', '镜像'],
|
||
renderRow: (s, opts) => (
|
||
<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>
|
||
),
|
||
},
|
||
daemonsets: {
|
||
title: 'DaemonSets', subtitle: '守护进程集', icon: Boxes,
|
||
columns: ['名称', '命名空间', '期望', '当前', '就绪', '镜像'],
|
||
renderRow: (d, opts) => (
|
||
<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>
|
||
),
|
||
},
|
||
serviceaccounts: {
|
||
title: 'ServiceAccounts', subtitle: '服务账号(RBAC 身份标识)', icon: Shield,
|
||
columns: ['名称', '命名空间', 'Secret数', '镜像拉取凭证', '创建时间', '操作'],
|
||
renderRow: (sa, opts) => (
|
||
<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-[var(--dg-text-muted)]">{sa.age || '-'}</TableCell>
|
||
<TableCell onClick={(e) => e.stopPropagation()}>
|
||
<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>
|
||
),
|
||
},
|
||
roles: {
|
||
title: 'Roles', subtitle: '命名空间级角色权限(RBAC)', icon: Shield,
|
||
columns: ['名称', '命名空间', '规则数', '权限范围', '创建时间', '操作'],
|
||
renderRow: (r, opts) => {
|
||
const verbs = r.verbs || [];
|
||
const resources = r.resources || [];
|
||
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-[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-[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-[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-[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-[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>
|
||
</TableRow>
|
||
);
|
||
},
|
||
},
|
||
rolebindings: {
|
||
title: 'RoleBindings', subtitle: '角色绑定关系(RBAC 授权)', icon: Shield,
|
||
columns: ['名称', '命名空间', '角色引用', '绑定主体', '创建时间', '操作'],
|
||
renderRow: (rb, opts) => {
|
||
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-[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-[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-[var(--dg-text-muted)] max-w-[200px] truncate" title={subjects.join(', ')}>
|
||
{subjStr || '-'}
|
||
</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-[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-[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>
|
||
</TableRow>
|
||
);
|
||
},
|
||
},
|
||
};
|
||
|
||
export default function NamespaceResourcePage() {
|
||
const { clusterCode, namespace } = useParams<{ clusterCode: string; namespace: string }>();
|
||
const location = useLocation();
|
||
const navigate = useNavigate();
|
||
const { toast } = useToast();
|
||
const confirm = useConfirm();
|
||
const type = useMemo<ResourceType>(() => {
|
||
const segments = location.pathname.split('/');
|
||
const last = segments[segments.length - 1];
|
||
if (last && RESOURCE_CONFIG[last as ResourceType]) return last as ResourceType;
|
||
if (segments.length >= 2) {
|
||
const secondLast = segments[segments.length - 2];
|
||
if (secondLast && RESOURCE_CONFIG[secondLast as ResourceType]) return secondLast as ResourceType;
|
||
}
|
||
return 'deployments';
|
||
}, [location.pathname]);
|
||
const config = RESOURCE_CONFIG[type];
|
||
|
||
const [clusters, setClusters] = useState<any[]>([]);
|
||
const [namespaces, setNamespaces] = useState<string[]>([]);
|
||
const [data, setData] = useState<any[]>([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [search, setSearch] = useState('');
|
||
const [yamlItem, setYamlItem] = useState<{resource: string; name: string; editMode?: boolean} | null>(null);
|
||
const [visualItem, setVisualItem] = useState<{ resource: string; name: string; isCreate?: boolean } | null>(null);
|
||
const [showYamlImport, setShowYamlImport] = useState(false);
|
||
|
||
const [terminalPod, setTerminalPod] = useState<any>(null);
|
||
const [fileBrowserPod, setFileBrowserPod] = useState<any>(null);
|
||
const [podDetail, setPodDetail] = useState<any>(null);
|
||
|
||
useEffect(() => {
|
||
kubernetesApi.getClusters().then(( res: any) => {
|
||
const list = res?.list || [];
|
||
setClusters(list);
|
||
if (!clusterCode && list.length > 0) {
|
||
const firstCode = list[0].code || String(list[0].id);
|
||
navigate(`/kubernetes/${firstCode}/namespace/${namespace || 'default'}/${type}`, { replace: true });
|
||
}
|
||
}).catch(() => setClusters([]));
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!clusterCode) return;
|
||
kubernetesApi.getNamespaces(clusterCode).then(( res: any) => {
|
||
const nsList: string[] = Array.isArray(res) ? res : (res?.list || res?.data || []);
|
||
setNamespaces(nsList);
|
||
if (!namespace && nsList.length > 0) {
|
||
navigate(`/kubernetes/${clusterCode}/namespace/${nsList[0]}/${type}`, { replace: true });
|
||
}
|
||
}).catch(() => setNamespaces([]));
|
||
}, [clusterCode]);
|
||
|
||
const loadData = useCallback(async () => {
|
||
if (!clusterCode || !namespace) return;
|
||
setLoading(true);
|
||
try {
|
||
let res: Record<string, any> = [];
|
||
switch (type) {
|
||
case 'deployments': res = await kubernetesApi.getNamespaceDeployments(clusterCode, namespace).catch(() => []); break;
|
||
case 'pods': res = await kubernetesApi.getNamespacePods(clusterCode, namespace).catch(() => []); break;
|
||
case 'services': res = await kubernetesApi.getNamespaceServices(clusterCode, namespace).catch(() => []); break;
|
||
case 'ingresses': res = await kubernetesApi.getNamespaceIngresses(clusterCode, namespace).catch(() => []); break;
|
||
case 'configmaps': res = await kubernetesApi.getNamespaceConfigMaps(clusterCode, namespace).catch(() => []); break;
|
||
case 'secrets': res = await kubernetesApi.getNamespaceSecrets(clusterCode, namespace).catch(() => []); break;
|
||
case 'hpas': res = await kubernetesApi.getHPAs(clusterCode, namespace).catch(() => []); break;
|
||
case 'cronjobs': res = await kubernetesApi.getCronJobs(clusterCode, namespace).catch(() => []); break;
|
||
case 'pvcs': res = await kubernetesApi.getPVCs(clusterCode, namespace).catch(() => []); break;
|
||
case 'statefulsets': res = await kubernetesApi.getStatefulSets(clusterCode, namespace).catch(() => []); break;
|
||
case 'daemonsets': res = await kubernetesApi.getDaemonSets(clusterCode, namespace).catch(() => []); break;
|
||
case 'serviceaccounts': res = await kubernetesApi.getServiceAccounts(clusterCode, namespace).catch(() => []); break;
|
||
case 'roles': res = await kubernetesApi.getRoles(clusterCode, namespace).catch(() => []); break;
|
||
case 'rolebindings': res = await kubernetesApi.getRoleBindings(clusterCode, namespace).catch(() => []); break;
|
||
}
|
||
setData(Array.isArray(res) ? res : (res?.list || res?.data || []));
|
||
} catch { setData([]); } finally { setLoading(false); }
|
||
}, [clusterCode, namespace, type]);
|
||
|
||
useEffect(() => { loadData(); }, [loadData]);
|
||
|
||
const filtered = data.filter(( d: any) => (d.name || '').toLowerCase().includes(search.toLowerCase()));
|
||
|
||
const handleDetail = ( item: any) => {
|
||
if (type === 'deployments') {
|
||
// Kuboard 风格双模式路由(默认 view 模式)
|
||
navigate(`/kubernetes/${clusterCode}/namespace/${namespace}/workload/view/Deployment/${item.name}`);
|
||
} else if (type === 'statefulsets') {
|
||
navigate(`/kubernetes/${clusterCode}/namespace/${namespace}/workload/view/StatefulSet/${item.name}`);
|
||
} else if (type === 'daemonsets') {
|
||
navigate(`/kubernetes/${clusterCode}/namespace/${namespace}/workload/view/DaemonSet/${item.name}`);
|
||
}
|
||
};
|
||
|
||
const handleYaml = (name: string) => setYamlItem({ resource: type, name });
|
||
// 网络/配置/存储走可视化编辑器,YAML 作为高级入口 // Logan.Li
|
||
const handleEdit = (name: string) => {
|
||
if (VISUAL_RESOURCE_TYPES.includes(type)) {
|
||
setVisualItem({ resource: type, name });
|
||
} else {
|
||
setYamlItem({ resource: type, name, editMode: true });
|
||
}
|
||
};
|
||
const handleCreate = () => {
|
||
if (type === 'deployments') {
|
||
navigate(`/kubernetes/${clusterCode}/namespace/${namespace}/workload/edit/Deployment/_create`);
|
||
return;
|
||
}
|
||
setVisualItem({ resource: type, name: '', isCreate: true });
|
||
};
|
||
const handleExportCSV = () => kubernetesApi.exportAsCSV(`${type}-${clusterCode}-${namespace}.csv`, filtered);
|
||
const handleExportJSON = () => kubernetesApi.exportAsJSON(`${type}-${clusterCode}-${namespace}.json`, filtered);
|
||
const handleExportYaml = async () => {
|
||
if (!clusterCode || !namespace) return;
|
||
try {
|
||
await kubernetesApi.exportNamespaceResources(clusterCode, namespace);
|
||
toast({ title: '导出完成', description: `${namespace} 资源 YAML 已开始下载` });
|
||
} catch (e) {
|
||
toast({ title: '导出失败', description: (e as Error)?.message || String(e), variant: 'destructive' });
|
||
}
|
||
};
|
||
|
||
// Pod 删除 // Logan.Li
|
||
const handleDeletePod = async (p: any) => {
|
||
const ok = await confirm({
|
||
title: '删除 Pod',
|
||
description: '确认删除 Pod "' + (p.name || '') + '"?操作不可恢复。',
|
||
confirmText: '删除',
|
||
});
|
||
if (!ok) return;
|
||
try {
|
||
await kubernetesApi.deletePod(clusterCode || '', namespace || '', p.name);
|
||
toast({ title: 'Pod 已删除' });
|
||
loadData();
|
||
} catch (e) {
|
||
toast({ title: '删除失败', description: (e as any)?.message || String(e), variant: 'destructive' });
|
||
}
|
||
};
|
||
|
||
if (!config) return <div className="text-[var(--dg-text-secondary)] p-8 text-center">未知资源类型: {type}</div>;
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
{yamlItem && (
|
||
<YamlViewer
|
||
clusterCode={clusterCode || ''}
|
||
namespace={namespace || ''}
|
||
resource={yamlItem.resource}
|
||
name={yamlItem.name}
|
||
onClose={() => setYamlItem(null)}
|
||
onSaved={loadData}
|
||
autoEdit={yamlItem.editMode === true}
|
||
/>
|
||
)}
|
||
{showYamlImport && (
|
||
<YamlImportDialog
|
||
clusterCode={clusterCode || ''}
|
||
onClose={() => setShowYamlImport(false)}
|
||
onApplied={loadData}
|
||
/>
|
||
)}
|
||
{visualItem && (
|
||
<ResourceVisualEditor
|
||
clusterCode={clusterCode || ''}
|
||
namespace={namespace || ''}
|
||
resource={visualItem.resource}
|
||
name={visualItem.name}
|
||
isCreate={visualItem.isCreate}
|
||
onClose={() => setVisualItem(null)}
|
||
onSaved={loadData}
|
||
/>
|
||
)}
|
||
|
||
<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-[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-[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-[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-[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-[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-[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-[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-[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-[var(--dg-blue)] hover:opacity-90" onClick={handleCreate}>
|
||
<Plus className="h-3 w-3 mr-1" />新建
|
||
</Button>
|
||
</>
|
||
)}
|
||
</div>
|
||
</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-[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-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||
<CardContent className="p-0">
|
||
<Table>
|
||
<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-[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-[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) }))
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* ============ Pod 终端 ============ */}
|
||
<PodExecTerminal
|
||
open={!!terminalPod}
|
||
onOpenChange={(open) => { if (!open) setTerminalPod(null); }}
|
||
clusterId={clusters[0]?.id || clusters[0]?.code || clusterCode || ''}
|
||
namespace={namespace || ''}
|
||
podName={terminalPod?.name || ''}
|
||
containers={(terminalPod?.containers || []).map(( c: any) => c.name).filter(Boolean)}
|
||
/>
|
||
{/* ============ Pod 文件浏览器 ============ */}
|
||
<PodFileBrowser
|
||
open={!!fileBrowserPod}
|
||
onOpenChange={(open) => { if (!open) setFileBrowserPod(null); }}
|
||
clusterCode={clusterCode || ''}
|
||
namespace={namespace || ''}
|
||
podName={fileBrowserPod?.name || ''}
|
||
containers={(fileBrowserPod?.containers || []).map(( c: any) => c.name).filter(Boolean)}
|
||
/>
|
||
{/* ============ Pod 详情弹窗 ============ */}
|
||
{podDetail && (
|
||
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center" onClick={() => setPodDetail(null)}>
|
||
<div className="bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-lg w-[680px] max-h-[85vh] overflow-y-auto" onClick={e => e.stopPropagation()}>
|
||
<div className="flex items-center justify-between px-5 py-4 border-b border-[var(--dg-border)]">
|
||
<h3 className="text-base font-semibold text-[var(--dg-text-primary)] flex items-center gap-2">
|
||
<Container className="h-4 w-4 text-[var(--dg-blue)]" />
|
||
Pod 详情
|
||
</h3>
|
||
<button className="text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]" onClick={() => setPodDetail(null)}>✕</button>
|
||
</div>
|
||
<div className="p-5 space-y-5">
|
||
{/* 基本信息 */}
|
||
<div>
|
||
<h4 className="text-sm font-medium text-[var(--dg-text-primary)] mb-3">基本信息</h4>
|
||
{(() => {
|
||
const p = podDetail;
|
||
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-[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">
|
||
<div>
|
||
<span className="text-xs text-[var(--dg-text-muted)]">名称</span>
|
||
<p className="text-sm text-[var(--dg-text-primary)] font-mono">{p.name || '-'}</p>
|
||
</div>
|
||
<div>
|
||
<span className="text-xs text-[var(--dg-text-muted)]">命名空间</span>
|
||
<p className="text-sm text-[var(--dg-text-primary)]">{p.namespace || '-'}</p>
|
||
</div>
|
||
<div>
|
||
<span className="text-xs text-[var(--dg-text-muted)]">状态</span>
|
||
<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>
|
||
<p className="text-sm text-[var(--dg-text-primary)]">{created ? new Date(created).toLocaleString('zh-CN') : '-'}</p>
|
||
</div>
|
||
<div>
|
||
<span className="text-xs text-[var(--dg-text-muted)]">节点</span>
|
||
<p className="text-sm text-[var(--dg-text-primary)]">{p.node_name || p.node || '-'}</p>
|
||
</div>
|
||
<div>
|
||
<span className="text-xs text-[var(--dg-text-muted)]">Pod IP</span>
|
||
<p className="text-sm text-[var(--dg-text-primary)] font-mono">{p.ip || p.pod_ip || '-'}</p>
|
||
</div>
|
||
<div className="col-span-2">
|
||
<span className="text-xs text-[var(--dg-text-muted)]">标签</span>
|
||
<div className="flex flex-wrap gap-1 mt-0.5">
|
||
{Object.keys(labels).length === 0 ? <span className="text-xs text-[var(--dg-text-muted)]">-</span>
|
||
: Object.entries(labels).map(([k, v]) => (
|
||
<Badge key={k} variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-muted)] text-[10px] font-mono">{k}: {String(v)}</Badge>
|
||
))
|
||
}
|
||
</div>
|
||
</div>
|
||
<div className="col-span-2">
|
||
<span className="text-xs text-[var(--dg-text-muted)]">注解</span>
|
||
<div className="flex flex-wrap gap-1 mt-0.5">
|
||
{Object.keys(annotations).length === 0 ? <span className="text-xs text-[var(--dg-text-muted)]">-</span>
|
||
: Object.entries(annotations).slice(0, 6).map(([k, v]) => (
|
||
<Badge key={k} variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-muted)] text-[10px] font-mono max-w-[300px] truncate" title={String(v)}>{k}</Badge>
|
||
))
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
})()}
|
||
</div>
|
||
|
||
{/* 现状详情(Conditions) */}
|
||
<div>
|
||
<h4 className="text-sm font-medium text-[var(--dg-text-primary)] mb-3">现状详情</h4>
|
||
{(() => {
|
||
const cond: any[] = podDetail.conditions || podDetail.pod_conditions || [];
|
||
if (cond.length === 0) return <p className="text-xs text-[var(--dg-text-muted)] py-2">暂无数据</p>;
|
||
return (
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||
<TableHead className="text-[var(--dg-text-muted)] text-xs">类型</TableHead>
|
||
<TableHead className="text-[var(--dg-text-muted)] text-xs">状态</TableHead>
|
||
<TableHead className="text-[var(--dg-text-muted)] text-xs">更新时间</TableHead>
|
||
<TableHead className="text-[var(--dg-text-muted)] text-xs">原因</TableHead>
|
||
<TableHead className="text-[var(--dg-text-muted)] text-xs">消息</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{cond.map((c: any, idx: number) => {
|
||
const type = c.type || c.Type || '-';
|
||
const st = c.status || c.Status || '-';
|
||
const ts = c.lastTransitionTime || c.LastTransitionTime || '';
|
||
const reason = c.reason || c.Reason || '-';
|
||
const msg = c.message || c.Message || '-';
|
||
const isTrue = st === 'True';
|
||
const isFalse = st === 'False';
|
||
return (
|
||
<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-[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>
|
||
<TableCell className="text-xs text-[var(--dg-text-muted)] max-w-[200px] truncate" title={msg}>{msg}</TableCell>
|
||
</TableRow>
|
||
);
|
||
})}
|
||
</TableBody>
|
||
</Table>
|
||
);
|
||
})()}
|
||
</div>
|
||
</div>
|
||
<div className="flex justify-end px-5 py-3 border-t border-[var(--dg-border)]">
|
||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={() => setPodDetail(null)}>关闭</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|