refactor(quality): Q9 前端巨型组件拆分 — NamespaceResourcePage 719L→300L (-58%), 提取 namespace-resource-config + PodDetailDialog 2 个独立文件

This commit is contained in:
attacker committed 2026-07-07 03:18:12 +08:00
1 parent 8a4dfb0db6
commit 1f8abc0bbc
5 files changed
+511 -435

No files matched your search

+1 -1
View File
@@ -129,7 +129,7 @@
| ~~`api.ts` 1324 行/352 方法~~ → 已拆分为 12 个域文件 (api/kubernetes.ts, deploy.ts, monitor.ts 等) | ✅ 已解决 | 前端 | ~~P0~~ done | | ~~`api.ts` 1324 行/352 方法~~ → 已拆分为 12 个域文件 (api/kubernetes.ts, deploy.ts, monitor.ts 等) | ✅ 已解决 | 前端 | ~~P0~~ done |
| 前端 **1026 处** `any` 类型 | 类型安全缺失,重构风险高 | 前端全局 | ~~P0~~ **done** (any 1076→40, unused 25→6, console 93→1, tsc --noEmit=0) | | 前端 **1026 处** `any` 类型 | 类型安全缺失,重构风险高 | 前端全局 | ~~P0~~ **done** (any 1076→40, unused 25→6, console 93→1, tsc --noEmit=0) |
| ~~K8s 双路由体系(集群域+Kuboard 域)~~ → code 路由统一为主,ID 路由标记 deprecated 向后兼容 | ✅ 已解决 | 后端 | ~~P1~~ done | | ~~K8s 双路由体系(集群域+Kuboard 域)~~ → code 路由统一为主,ID 路由标记 deprecated 向后兼容 | ✅ 已解决 | 后端 | ~~P1~~ done |
| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6+Q7+Q8 done: 最大组件 ResourceVisualEditor 882L→282L (-68%) + AgentCLI 814L→521L (-36%) + SystemSettingsTab 754L→173L (-77%),累计提取 14 个独立组件文件** | | 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6+Q7+Q8+Q9 done: 最大组件 ResourceVisualEditor 882L→282L (-68%) + AgentCLI 814L→521L (-36%) + SystemSettingsTab 754L→173L (-77%) + NamespaceResourcePage 719L→300L (-58%),累计提取 16 个独立组件文件** |
### 3.2 🟡 中优先级 ### 3.2 🟡 中优先级
+32
View File
@@ -480,6 +480,7 @@ services/api.ts (1324L)
| Q6 | 前端巨型组件拆分(ResourceVisualEditor 882L→282L,提取 5 个独立编辑器) | P0 | **done** | | Q6 | 前端巨型组件拆分(ResourceVisualEditor 882L→282L,提取 5 个独立编辑器) | P0 | **done** |
| Q7 | 前端巨型组件拆分(AgentCLI 814L→521L,提取工具栏+常量+空状态+滚动按钮) | P0 | **done** | | Q7 | 前端巨型组件拆分(AgentCLI 814L→521L,提取工具栏+常量+空状态+滚动按钮) | P0 | **done** |
| Q8 | 前端巨型组件拆分(SystemSettingsTab 754L→173L,提取通用/安全/LDAP 3 个子 Tab) | P0 | **done** | | Q8 | 前端巨型组件拆分(SystemSettingsTab 754L→173L,提取通用/安全/LDAP 3 个子 Tab) | P0 | **done** |
| Q9 | 前端巨型组件拆分(NamespaceResourcePage 719L→300L,提取 RESOURCE_CONFIG + PodDetailDialog) | P0 | **done** |
--- ---
@@ -710,3 +711,34 @@ pending → in_progress → done / blocked
- [x] 17/17 集成回归测试通过(零回归) - [x] 17/17 集成回归测试通过(零回归)
- [x] Docker 部署 healthy - [x] Docker 部署 healthy
- [x] 系统设置页 3 个 Tab 功能不变(通用/安全/LDAP 全部可切换) - [x] 系统设置页 3 个 Tab 功能不变(通用/安全/LDAP 全部可切换)
---
## Q9: 前端巨型组件拆分 — NamespaceResourcePage [done]
**问题**: `NamespaceResourcePage.tsx`(719 行)是 Q8 拆分后项目最大的业务组件之一,包含命名空间资源列表页面的全部逻辑:13 种资源类型的 `RESOURCE_CONFIG` 声明式行渲染器(Deployments/Pods/Services/Ingresses/ConfigMaps/Secrets/HPAs/CronJobs/PVCs/StatefulSets/DaemonSets/ServiceAccounts/Roles/RoleBindings)、VisualActions 组件、类型常量数组、以及 Pod 详情弹窗(基本信息网格 + Conditions 表格)。单文件过大导致维护困难、代码导航低效。
**已完成 (2026-07-07)**:
将 RESOURCE_CONFIG 配置和 Pod 详情弹窗提取为独立文件:
| 新文件 | 行数 | 职责 |
|--------|------|------|
| `namespace-resource-config.tsx` | 321 | 13 种资源类型的列定义 + 行渲染器 + VisualActions + 类型常量 + ResourceRowOpts 接口 |
| `PodDetailDialog.tsx` | 144 | Pod 详情弹窗(基本信息 2 列网格 + Labels/Annotations Badge + Conditions 现状表格) |
主文件 `NamespaceResourcePage.tsx` 从 **719 行降至 300 行**(-58%),仅保留:状态管理(clusters/namespaces/data/yamlItem/visualItem/podDetail)、数据加载(loadData switch 13 分支)、事件处理(handleDetail/handleYaml/handleEdit/handleCreate/handleExportCSV/JSON/Yaml/handleDeletePod)、PageShell 布局(工具栏 + 搜索 + Table + Dialog 集成)。
**提取原则**: config 文件为纯声明式数据(`RESOURCE_CONFIG` 对象 + `VisualActions` 函数组件 + 类型/常量导出),主组件通过 import 使用;PodDetailDialog 为纯展示组件(接收 `pod` + `onClose` props)。零逻辑变更。
**验收标准**:
- [x] NamespaceResourcePage.tsx 从 719 行降至 **300 行**(-58%)
- [x] 2 个提取的文件全部 < 325 行(namespace-resource-config 321 行 + PodDetailDialog 144 行)
- [x] `tsc -b` 零错误
- [x] `npm run build` 零错误(✓ built in 15.88s)
- [x] ESLint errors 保持 **0**(清理 4 个 unused import: Table/TableBody/TableHead/TableHeader)
- [x] ESLint warnings 保持 **59**(5 个 react-refresh/only-export-components 符合既有模式)
- [x] `go build` 零错误
- [x] 17/17 集成回归测试通过(零回归)
- [x] Docker 部署 healthy
- [x] 命名空间资源列表页 13 种资源类型功能不变(全部可正常渲染/操作)
@@ -3,23 +3,24 @@ import { useState, useEffect, useCallback, useMemo } from 'react';
import { useParams, useNavigate, useLocation } from 'react-router-dom'; import { useParams, useNavigate, useLocation } from 'react-router-dom';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import { import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from '@/components/ui/table'; } from '@/components/ui/table';
import { import { RefreshCw, Search, Loader2, Download, Upload, Plus } from 'lucide-react';
RefreshCw, Search, Loader2, Boxes, Container,
Network, FileText, Shield, Activity, Clock,
Download, FileCode, Pencil, Terminal, HardDrive, Plus, Upload, Trash2, Eye,
} from 'lucide-react';
import { YamlViewer } from './YamlViewer'; import { YamlViewer } from './YamlViewer';
import { ResourceVisualEditor } from './ResourceVisualEditor'; import { ResourceVisualEditor } from './ResourceVisualEditor';
import { YamlImportDialog } from './YamlImportDialog'; import { YamlImportDialog } from './YamlImportDialog';
import { PodExecTerminal } from './PodExecTerminal'; import { PodExecTerminal } from './PodExecTerminal';
import { PodFileBrowser } from './PodFileBrowser'; import { PodFileBrowser } from './PodFileBrowser';
import { PodDetailDialog } from './PodDetailDialog';
import {
RESOURCE_CONFIG, VISUAL_RESOURCE_TYPES, DEPLOYMENT_LIST_TYPES,
EXPORT_YAML_TYPES, BULK_EXPORT_TYPES,
type ResourceType,
} from './namespace-resource-config';
import { kubernetesApi } from '@/api'; import { kubernetesApi } from '@/api';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { useConfirm } from '@/components/ui/confirm-dialog'; import { useConfirm } from '@/components/ui/confirm-dialog';
@@ -27,304 +28,6 @@ import { PageShell } from '@/components/layout/PageShell';
import { ContentPanel } from '@/components/layout/ContentPanel'; import { ContentPanel } from '@/components/layout/ContentPanel';
import i18n from '@/i18n'; import i18n from '@/i18n';
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={i18n.t('k8s.resource.visualEditor')}
onClick={() => opts.onEdit?.(name)}><Pencil className="h-3.5 w-3.5 mr-1" /> {i18n.t('common.edit')} </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: i18n.t('kubernetes.namespaceresour.s35c744fb'), icon: Boxes,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.capacitypagetsx.s0428b36a'), i18n.t('kubernetes.container.image')],
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: i18n.t('kubernetes.namespaceresour.s780b7dd3'), icon: Container,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('system.terminalStatus'), i18n.t('kubernetes.node.title'), 'CPU', i18n.t('kubernetes.pod.memory'), i18n.t('agent.agentServiceRestart'), i18n.t('kubernetes.container.image'), i18n.t('system.auditAction')],
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={i18n.t('kubernetes.pod.terminal')}
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={i18n.t('k8s.pods.action.file')}
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={i18n.t('kubernetes.storage.delete2')}
onClick={() => opts.onDeletePod?.(p)}><Trash2 className="h-3.5 w-3.5" /></Button>
</div>
</TableCell>
</TableRow>
);
},
},
services: {
title: 'Services', subtitle: i18n.t('kubernetes.namespaceresour.s5e76b605'), icon: Network,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('system.credentialType'), 'Cluster IP', i18n.t('system.ldapPort'), i18n.t('system.auditAction')],
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: i18n.t('kubernetes.namespaceresour.s8d501473'), icon: Network,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), 'Hosts', 'TLS', i18n.t('system.auditAction')],
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: i18n.t('kubernetes.namespaceovervi.s0986a6e7'), icon: FileText,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.namespaceresour.sbd1fa02a'), i18n.t('system.auditAction')],
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: i18n.t('kubernetes.namespaceresour.sd1424af7'), icon: Shield,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('system.credentialType'), i18n.t('system.auditAction')],
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: i18n.t('kubernetes.namespaceresour.s9abc8d19'), icon: Activity,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('monitor.alertEvents.col.target'), i18n.t('k8s.hpaCronHpa.col.minMax'), '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: i18n.t('automation.cronJobs'), icon: Clock,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.workload.scheduling'), i18n.t('system.terminalStatus'), i18n.t('automation.scriptLastExec')],
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 ? i18n.t('kubernetes.deployment.paused') : i18n.t('agent.agentServiceRunning')}</Badge></TableCell>
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{cj.last_schedule || '-'}</TableCell>
</TableRow>
),
},
pvcs: {
title: i18n.t('kubernetes.namespaceresour.s18c90062'), subtitle: 'PersistentVolumeClaim', icon: Boxes,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('system.terminalStatus'), i18n.t('kubernetes.storage.pvcSize'), 'StorageClass', i18n.t('system.auditAction')],
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: i18n.t('kubernetes.namespaceresour.sbfeb44d8'), icon: Boxes,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.capacitypagetsx.s0428b36a'), i18n.t('kubernetes.pod.ready'), i18n.t('kubernetes.container.image')],
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: i18n.t('kubernetes.namespaceresour.se1c074e2'), icon: Boxes,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('monitor.siteProtocol.http.expectedPrefix'), i18n.t('cloud.current'), i18n.t('kubernetes.pod.ready'), i18n.t('kubernetes.container.image')],
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: i18n.t('kubernetes.namespaceresour.s645c9a3d'), icon: Shield,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.namespaceresour.sbaf7ed70'), i18n.t('kubernetes.namespaceresour.s260c11ec'), i18n.t('system.userCreatedAt'), i18n.t('system.auditAction')],
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={i18n.t('kubernetes.namespaceresour.s4bde086b')}
onClick={() => opts.onYaml?.(sa.name)}><FileCode className="h-3.5 w-3.5" /></Button>
</TableCell>
</TableRow>
),
},
roles: {
title: 'Roles', subtitle: i18n.t('kubernetes.namespaceresour.s8905a680'), icon: Shield,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.namespaceresour.secde032d'), i18n.t('system.apiKeyScope'), i18n.t('system.userCreatedAt'), i18n.t('system.auditAction')],
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={i18n.t('kubernetes.namespaceresour.s4bde086b')}
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={i18n.t('common.edit')}
onClick={() => opts.onEdit?.(r.name)}><Pencil className="h-3.5 w-3.5" /></Button>
</div>
</TableCell>
</TableRow>
);
},
},
rolebindings: {
title: 'RoleBindings', subtitle: i18n.t('kubernetes.namespaceresour.sca10fe5c'), icon: Shield,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.namespaceresour.s227e5e28'), i18n.t('kubernetes.namespaceresour.s2667c7d8'), i18n.t('system.userCreatedAt'), i18n.t('system.auditAction')],
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={i18n.t('kubernetes.namespaceresour.s4bde086b')}
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={i18n.t('common.edit')}
onClick={() => opts.onEdit?.(rb.name)}><Pencil className="h-3.5 w-3.5" /></Button>
</div>
</TableCell>
</TableRow>
);
},
},
};
export default function NamespaceResourcePage() { export default function NamespaceResourcePage() {
const { clusterCode, namespace } = useParams<{ clusterCode: string; namespace: string }>(); const { clusterCode, namespace } = useParams<{ clusterCode: string; namespace: string }>();
const location = useLocation(); const location = useLocation();
@@ -357,7 +60,7 @@ export default function NamespaceResourcePage() {
const [podDetail, setPodDetail] = useState<any>(null); const [podDetail, setPodDetail] = useState<any>(null);
useEffect(() => { useEffect(() => {
kubernetesApi.getClusters().then(( res: any) => { kubernetesApi.getClusters().then((res: any) => {
const list = res?.list || []; const list = res?.list || [];
setClusters(list); setClusters(list);
if (!clusterCode && list.length > 0) { if (!clusterCode && list.length > 0) {
@@ -370,7 +73,7 @@ export default function NamespaceResourcePage() {
useEffect(() => { useEffect(() => {
if (!clusterCode) return; if (!clusterCode) return;
kubernetesApi.getNamespaces(clusterCode).then(( res: any) => { kubernetesApi.getNamespaces(clusterCode).then((res: any) => {
const nsList: string[] = Array.isArray(res) ? res : (res?.list || res?.data || []); const nsList: string[] = Array.isArray(res) ? res : (res?.list || res?.data || []);
setNamespaces(nsList); setNamespaces(nsList);
if (!namespace && nsList.length > 0) { if (!namespace && nsList.length > 0) {
@@ -407,11 +110,10 @@ export default function NamespaceResourcePage() {
useEffect(() => { loadData(); }, [loadData]); useEffect(() => { loadData(); }, [loadData]);
const filtered = data.filter(( d: any) => (d.name || '').toLowerCase().includes(search.toLowerCase())); const filtered = data.filter((d: any) => (d.name || '').toLowerCase().includes(search.toLowerCase()));
const handleDetail = ( item: any) => { const handleDetail = (item: any) => {
if (type === 'deployments') { if (type === 'deployments') {
// Kuboard 风格双模式路由(默认 view 模式)
navigate(`/kubernetes/${clusterCode}/namespace/${namespace}/workload/view/Deployment/${item.name}`); navigate(`/kubernetes/${clusterCode}/namespace/${namespace}/workload/view/Deployment/${item.name}`);
} else if (type === 'statefulsets') { } else if (type === 'statefulsets') {
navigate(`/kubernetes/${clusterCode}/namespace/${namespace}/workload/view/StatefulSet/${item.name}`); navigate(`/kubernetes/${clusterCode}/namespace/${namespace}/workload/view/StatefulSet/${item.name}`);
@@ -567,7 +269,7 @@ export default function NamespaceResourcePage() {
) : filtered.length === 0 ? ( ) : 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> <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) })) 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> </TableBody>
</Table> </Table>
@@ -580,7 +282,7 @@ export default function NamespaceResourcePage() {
clusterId={clusters[0]?.id || clusters[0]?.code || clusterCode || ''} clusterId={clusters[0]?.id || clusters[0]?.code || clusterCode || ''}
namespace={namespace || ''} namespace={namespace || ''}
podName={terminalPod?.name || ''} podName={terminalPod?.name || ''}
containers={(terminalPod?.containers || []).map(( c: any) => c.name).filter(Boolean)} containers={(terminalPod?.containers || []).map((c: any) => c.name).filter(Boolean)}
/> />
{/* ============ Pod 文件浏览器 ============ */} {/* ============ Pod 文件浏览器 ============ */}
<PodFileBrowser <PodFileBrowser
@@ -589,131 +291,10 @@ export default function NamespaceResourcePage() {
clusterCode={clusterCode || ''} clusterCode={clusterCode || ''}
namespace={namespace || ''} namespace={namespace || ''}
podName={fileBrowserPod?.name || ''} podName={fileBrowserPod?.name || ''}
containers={(fileBrowserPod?.containers || []).map(( c: any) => c.name).filter(Boolean)} containers={(fileBrowserPod?.containers || []).map((c: any) => c.name).filter(Boolean)}
/> />
{/* ============ Pod 详情弹窗 ============ */} {/* ============ Pod 详情弹窗 ============ */}
{podDetail && ( <PodDetailDialog pod={podDetail} onClose={() => setPodDetail(null)} />
<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"> {i18n.t('k8s.editMode.basic')} </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)]"> {i18n.t('kubernetes.secret.secretName')} </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)]"> {i18n.t('kubernetes.namespace.title')} </span>
<p className="text-sm text-[var(--dg-text-primary)]">{p.namespace || '-'}</p>
</div>
<div>
<span className="text-xs text-[var(--dg-text-muted)]"> {i18n.t('system.terminalStatus')} </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)]"> {i18n.t('system.userCreatedAt')} </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)]"> {i18n.t('kubernetes.node.title')} </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)]"> {i18n.t('kubernetes.namespace.labels')} </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)]"> {i18n.t('kubernetes.namespace.annotations')} </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"> {i18n.t('kubernetes.namespaceresour.s1d8d6e2e')} </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"> {i18n.t('aiops.assistantNoData')} </p>;
return (
<Table>
<TableHeader>
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
<TableHead className="text-[var(--dg-text-muted)] text-xs"> {i18n.t('system.credentialType')} </TableHead>
<TableHead className="text-[var(--dg-text-muted)] text-xs"> {i18n.t('system.terminalStatus')} </TableHead>
<TableHead className="text-[var(--dg-text-muted)] text-xs"> {i18n.t('automation.scriptUpdatedAt')} </TableHead>
<TableHead className="text-[var(--dg-text-muted)] text-xs"> {i18n.t('kubernetes.event.eventReason')} </TableHead>
<TableHead className="text-[var(--dg-text-muted)] text-xs"> {i18n.t('kubernetes.event.eventMessage')} </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)}> {i18n.t('agent.portClosed')} </Button>
</div>
</div>
</div>
)}
</PageShell> </PageShell>
); );
} }
@@ -0,0 +1,144 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Container } from 'lucide-react';
import i18n from '@/i18n';
interface PodDetailDialogProps {
pod: any;
onClose: () => void;
}
/**
* Pod 详情弹窗 — 基本信息网格 + Conditions 现状详情表格
* 从 NamespaceResourcePage 提取的独立组件,零逻辑变更
*/
export function PodDetailDialog({ pod, onClose }: PodDetailDialogProps) {
if (!pod) return null;
const labels = pod.labels || pod.metadata?.labels || {};
const annotations = pod.annotations || pod.metadata?.annotations || {};
const created = pod.creationTimestamp || pod.created_at || pod.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 = pod.status || 'Running';
const cond: any[] = pod.conditions || pod.pod_conditions || [];
return (
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center" onClick={onClose}>
<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={onClose}>✕</button>
</div>
<div className="p-5 space-y-5">
{/* 基本信息 */}
<div>
<h4 className="text-sm font-medium text-[var(--dg-text-primary)] mb-3"> {i18n.t('k8s.editMode.basic')} </h4>
<div className="grid grid-cols-2 gap-x-6 gap-y-2.5">
<div>
<span className="text-xs text-[var(--dg-text-muted)]"> {i18n.t('kubernetes.secret.secretName')} </span>
<p className="text-sm text-[var(--dg-text-primary)] font-mono">{pod.name || '-'}</p>
</div>
<div>
<span className="text-xs text-[var(--dg-text-muted)]"> {i18n.t('kubernetes.namespace.title')} </span>
<p className="text-sm text-[var(--dg-text-primary)]">{pod.namespace || '-'}</p>
</div>
<div>
<span className="text-xs text-[var(--dg-text-muted)]"> {i18n.t('system.terminalStatus')} </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)]"> {i18n.t('system.userCreatedAt')} </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)]"> {i18n.t('kubernetes.node.title')} </span>
<p className="text-sm text-[var(--dg-text-primary)]">{pod.node_name || pod.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">{pod.ip || pod.pod_ip || '-'}</p>
</div>
<div className="col-span-2">
<span className="text-xs text-[var(--dg-text-muted)]"> {i18n.t('kubernetes.namespace.labels')} </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)]"> {i18n.t('kubernetes.namespace.annotations')} </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"> {i18n.t('kubernetes.namespaceresour.s1d8d6e2e')} </h4>
{cond.length === 0 ? (
<p className="text-xs text-[var(--dg-text-muted)] py-2"> {i18n.t('aiops.assistantNoData')} </p>
) : (
<Table>
<TableHeader>
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
<TableHead className="text-[var(--dg-text-muted)] text-xs"> {i18n.t('system.credentialType')} </TableHead>
<TableHead className="text-[var(--dg-text-muted)] text-xs"> {i18n.t('system.terminalStatus')} </TableHead>
<TableHead className="text-[var(--dg-text-muted)] text-xs"> {i18n.t('automation.scriptUpdatedAt')} </TableHead>
<TableHead className="text-[var(--dg-text-muted)] text-xs"> {i18n.t('kubernetes.event.eventReason')} </TableHead>
<TableHead className="text-[var(--dg-text-muted)] text-xs"> {i18n.t('kubernetes.event.eventMessage')} </TableHead>
</TableRow>
</TableHeader>
<TableBody>
{cond.map((c: any, idx: number) => {
const ctype = c.type || c.Type || '-';
const cst = c.status || c.Status || '-';
const ts = c.lastTransitionTime || c.LastTransitionTime || '';
const reason = c.reason || c.Reason || '-';
const msg = c.message || c.Message || '-';
const isTrue = cst === 'True';
const isFalse = cst === 'False';
return (
<TableRow key={idx} className="border-[var(--dg-border)]">
<TableCell className="text-xs text-[var(--dg-text-primary)] font-medium">{ctype}</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)]'}>{cst}</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={onClose}> {i18n.t('agent.portClosed')} </Button>
</div>
</div>
</div>
);
}
@@ -0,0 +1,319 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { TableCell, TableRow } from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Boxes, Container, Network, FileText, Shield, Activity, Clock,
FileCode, Pencil, Terminal, HardDrive, Trash2, Eye,
} from 'lucide-react';
import i18n from '@/i18n';
export type ResourceType =
| 'deployments' | 'pods' | 'services' | 'ingresses'
| 'configmaps' | 'secrets' | 'hpas' | 'cronjobs'
| 'pvcs' | 'statefulsets' | 'daemonsets'
| 'serviceaccounts' | 'roles' | 'rolebindings';
// 支持可视化编辑/新建的资源类型 // Logan.Li
export const VISUAL_RESOURCE_TYPES: ResourceType[] = ['services', 'ingresses', 'configmaps', 'secrets', 'pvcs'];
/** Deployment 列表:新建 + YAML 导入,不做批量导出 // Logan.Li */
export const DEPLOYMENT_LIST_TYPES: ResourceType[] = ['deployments'];
/** 支持命名空间 YAML 批量导出(对齐集群导出)// Logan.Li */
export const EXPORT_YAML_TYPES: ResourceType[] = ['services', 'ingresses', 'configmaps', 'secrets'];
/** 列表页保留 CSV/JSON 导出的配置类资源 // Logan.Li */
export const BULK_EXPORT_TYPES: ResourceType[] = ['services', 'ingresses', 'configmaps', 'secrets', 'pvcs'];
export interface ResourceRowOpts {
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;
}
export 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={i18n.t('k8s.resource.visualEditor')}
onClick={() => opts.onEdit?.(name)}><Pencil className="h-3.5 w-3.5 mr-1" /> {i18n.t('common.edit')} </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>
);
export const RESOURCE_CONFIG: Record<ResourceType, {
title: string;
subtitle: string;
icon: React.ComponentType<any>;
columns: string[];
renderRow: (item: any, opts: ResourceRowOpts) => React.ReactNode;
}> = {
deployments: {
title: 'Deployments', subtitle: i18n.t('kubernetes.namespaceresour.s35c744fb'), icon: Boxes,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.capacitypagetsx.s0428b36a'), i18n.t('kubernetes.container.image')],
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: i18n.t('kubernetes.namespaceresour.s780b7dd3'), icon: Container,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('system.terminalStatus'), i18n.t('kubernetes.node.title'), 'CPU', i18n.t('kubernetes.pod.memory'), i18n.t('agent.agentServiceRestart'), i18n.t('kubernetes.container.image'), i18n.t('system.auditAction')],
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={i18n.t('kubernetes.pod.terminal')}
onClick={() => opts.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={i18n.t('k8s.pods.action.file')}
onClick={() => opts.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={i18n.t('kubernetes.storage.delete2')}
onClick={() => opts.onDeletePod?.(p)}><Trash2 className="h-3.5 w-3.5" /></Button>
</div>
</TableCell>
</TableRow>
);
},
},
services: {
title: 'Services', subtitle: i18n.t('kubernetes.namespaceresour.s5e76b605'), icon: Network,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('system.credentialType'), 'Cluster IP', i18n.t('system.ldapPort'), i18n.t('system.auditAction')],
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: i18n.t('kubernetes.namespaceresour.s8d501473'), icon: Network,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), 'Hosts', 'TLS', i18n.t('system.auditAction')],
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: i18n.t('kubernetes.namespaceovervi.s0986a6e7'), icon: FileText,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.namespaceresour.sbd1fa02a'), i18n.t('system.auditAction')],
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: i18n.t('kubernetes.namespaceresour.sd1424af7'), icon: Shield,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('system.credentialType'), i18n.t('system.auditAction')],
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: i18n.t('kubernetes.namespaceresour.s9abc8d19'), icon: Activity,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('monitor.alertEvents.col.target'), i18n.t('k8s.hpaCronHpa.col.minMax'), '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: i18n.t('automation.cronJobs'), icon: Clock,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.workload.scheduling'), i18n.t('system.terminalStatus'), i18n.t('automation.scriptLastExec')],
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 ? i18n.t('kubernetes.deployment.paused') : i18n.t('agent.agentServiceRunning')}</Badge></TableCell>
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{cj.last_schedule || '-'}</TableCell>
</TableRow>
),
},
pvcs: {
title: i18n.t('kubernetes.namespaceresour.s18c90062'), subtitle: 'PersistentVolumeClaim', icon: Boxes,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('system.terminalStatus'), i18n.t('kubernetes.storage.pvcSize'), 'StorageClass', i18n.t('system.auditAction')],
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: i18n.t('kubernetes.namespaceresour.sbfeb44d8'), icon: Boxes,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.capacitypagetsx.s0428b36a'), i18n.t('kubernetes.pod.ready'), i18n.t('kubernetes.container.image')],
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: i18n.t('kubernetes.namespaceresour.se1c074e2'), icon: Boxes,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('monitor.siteProtocol.http.expectedPrefix'), i18n.t('cloud.current'), i18n.t('kubernetes.pod.ready'), i18n.t('kubernetes.container.image')],
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: i18n.t('kubernetes.namespaceresour.s645c9a3d'), icon: Shield,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.namespaceresour.sbaf7ed70'), i18n.t('kubernetes.namespaceresour.s260c11ec'), i18n.t('system.userCreatedAt'), i18n.t('system.auditAction')],
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={i18n.t('kubernetes.namespaceresour.s4bde086b')}
onClick={() => opts.onYaml?.(sa.name)}><FileCode className="h-3.5 w-3.5" /></Button>
</TableCell>
</TableRow>
),
},
roles: {
title: 'Roles', subtitle: i18n.t('kubernetes.namespaceresour.s8905a680'), icon: Shield,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.namespaceresour.secde032d'), i18n.t('system.apiKeyScope'), i18n.t('system.userCreatedAt'), i18n.t('system.auditAction')],
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={i18n.t('kubernetes.namespaceresour.s4bde086b')}
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={i18n.t('common.edit')}
onClick={() => opts.onEdit?.(r.name)}><Pencil className="h-3.5 w-3.5" /></Button>
</div>
</TableCell>
</TableRow>
);
},
},
rolebindings: {
title: 'RoleBindings', subtitle: i18n.t('kubernetes.namespaceresour.sca10fe5c'), icon: Shield,
columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.namespaceresour.s227e5e28'), i18n.t('kubernetes.namespaceresour.s2667c7d8'), i18n.t('system.userCreatedAt'), i18n.t('system.auditAction')],
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={i18n.t('kubernetes.namespaceresour.s4bde086b')}
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={i18n.t('common.edit')}
onClick={() => opts.onEdit?.(rb.name)}><Pencil className="h-3.5 w-3.5" /></Button>
</div>
</TableCell>
</TableRow>
);
},
},
};