diff --git a/PROJECT_EVALUATION.md b/PROJECT_EVALUATION.md index 6242e39..3ba434b 100644 --- a/PROJECT_EVALUATION.md +++ b/PROJECT_EVALUATION.md @@ -129,7 +129,7 @@ | ~~`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) | | ~~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 🟡 中优先级 diff --git a/ROADMAP.md b/ROADMAP.md index 5a5b019..922790b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -480,6 +480,7 @@ services/api.ts (1324L) | Q6 | 前端巨型组件拆分(ResourceVisualEditor 882L→282L,提取 5 个独立编辑器) | P0 | **done** | | Q7 | 前端巨型组件拆分(AgentCLI 814L→521L,提取工具栏+常量+空状态+滚动按钮) | 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] Docker 部署 healthy - [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 种资源类型功能不变(全部可正常渲染/操作) diff --git a/frontend/src/components/kubernetes/NamespaceResourcePage.tsx b/frontend/src/components/kubernetes/NamespaceResourcePage.tsx index 7e108ec..7841e30 100644 --- a/frontend/src/components/kubernetes/NamespaceResourcePage.tsx +++ b/frontend/src/components/kubernetes/NamespaceResourcePage.tsx @@ -3,23 +3,24 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import { useParams, useNavigate, useLocation } from 'react-router-dom'; 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, - Download, FileCode, Pencil, Terminal, HardDrive, Plus, Upload, Trash2, Eye, -} from 'lucide-react'; +import { RefreshCw, Search, Loader2, Download, Upload, Plus } from 'lucide-react'; import { YamlViewer } from './YamlViewer'; import { ResourceVisualEditor } from './ResourceVisualEditor'; import { YamlImportDialog } from './YamlImportDialog'; import { PodExecTerminal } from './PodExecTerminal'; 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 { useToast } from '@/hooks/use-toast'; import { useConfirm } from '@/components/ui/confirm-dialog'; @@ -27,304 +28,6 @@ import { PageShell } from '@/components/layout/PageShell'; import { ContentPanel } from '@/components/layout/ContentPanel'; 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 } }) => ( -
- - -
-); - -const RESOURCE_CONFIG: Record; - 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) => ( - opts.onDetail?.(d)}> - {d.name} - {d.namespace} - - {(() => { - const ready = d.ready_replicas || 0; const total = d.replicas || 0; - const pct = total > 0 ? (ready / total) * 100 : 0; const done = ready === total; - return ( -
-
-
-
- {ready}/{total} -
- ); - })()} - - {d.image || '-'} - - ), - }, - 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 = { 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 ( - - opts.onPodDetail?.(p)}> -
- - {p.name} -
-
- {p.namespace} - {p.status || 'Unknown'} - {p.node_name || '-'} - {p.cpu_usage_m || '-'} - {p.mem_usage_mi || '-'} - 0 ? 'text-[var(--dg-orange)]' : 'text-[var(--dg-text-muted)]')}>{p.restart_count || 0} - {p.image || '-'} - -
- - - - -
-
-
- ); - }, - }, - 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) => ( - - {s.name} - {s.namespace} - {s.type || '-'} - {s.cluster_ip || '-'} - {s.ports || '-'} - e.stopPropagation()}> - - ), - }, - 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 ( - - {i.name} - {i.namespace} - {hosts.length > 0 ? hosts.join(', ') : '-'} - {i.tls ? 'HTTPS' : 'HTTP'} - e.stopPropagation()}> - - ); - }, - }, - 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) => ( - - {c.name} - {c.namespace} - {c.key_count || '-'} - e.stopPropagation()}> - - ), - }, - 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) => ( - - {s.name} - {s.namespace} - {s.type || '-'} - e.stopPropagation()}> - - ), - }, - 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) => ( - - {h.name} - {h.namespace} - {h.target_kind}/{h.target_name} - {h.min_replicas}/{h.max_replicas} - {h.cpu_percent ?? '-'}% - {h.mem_percent ?? '-'}% - - ), - }, - 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) => ( - - {cj.name} - {cj.namespace} - {cj.schedule || '-'} - {cj.suspend ? i18n.t('kubernetes.deployment.paused') : i18n.t('agent.agentServiceRunning')} - {cj.last_schedule || '-'} - - ), - }, - 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) => ( - - {p.name} - {p.namespace} - {p.phase || '-'} - {p.size || '-'} - {p.storage_class || '-'} - e.stopPropagation()}> - - ), - }, - 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) => ( - opts.onDetail?.(s)}> - {s.name} - {s.namespace} - {s.replicas || 0} - {s.ready || 0} - {s.image || '-'} - - ), - }, - 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) => ( - opts.onDetail?.(d)}> - {d.name} - {d.namespace} - {d.desired || 0} - {d.current || 0} - {d.ready || 0} - {d.image || '-'} - - ), - }, - 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) => ( - - {sa.name} - {sa.namespace} - {sa.secret_count ?? 0} - - {sa.image_pull_secrets && sa.image_pull_secrets.length > 0 ? sa.image_pull_secrets.join(', ') : '-'} - - {sa.age || '-'} - e.stopPropagation()}> - - - - ), - }, - 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 ( - - {r.name} - {r.namespace} - {r.rule_count ?? 0} - -
- verbs: {verbsStr || '-'} - resources: {resStr || '-'} -
-
- {r.age || '-'} - e.stopPropagation()}> -
- - -
-
-
- ); - }, - }, - 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 ( - - {rb.name} - {rb.namespace} - - - {rb.role_ref_kind || '-'}/{rb.role_ref_name || '-'} - - - - {subjStr || '-'} - - {rb.age || '-'} - e.stopPropagation()}> -
- - -
-
-
- ); - }, - }, -}; - export default function NamespaceResourcePage() { const { clusterCode, namespace } = useParams<{ clusterCode: string; namespace: string }>(); const location = useLocation(); @@ -357,7 +60,7 @@ export default function NamespaceResourcePage() { const [podDetail, setPodDetail] = useState(null); useEffect(() => { - kubernetesApi.getClusters().then(( res: any) => { + kubernetesApi.getClusters().then((res: any) => { const list = res?.list || []; setClusters(list); if (!clusterCode && list.length > 0) { @@ -370,7 +73,7 @@ export default function NamespaceResourcePage() { useEffect(() => { 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 || []); setNamespaces(nsList); if (!namespace && nsList.length > 0) { @@ -407,11 +110,10 @@ export default function NamespaceResourcePage() { 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') { - // 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}`); @@ -567,7 +269,7 @@ export default function NamespaceResourcePage() { ) : filtered.length === 0 ? ( 暂无 {config.title} ) : ( - 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) })) )} @@ -580,7 +282,7 @@ export default function NamespaceResourcePage() { clusterId={clusters[0]?.id || clusters[0]?.code || clusterCode || ''} namespace={namespace || ''} podName={terminalPod?.name || ''} - containers={(terminalPod?.containers || []).map(( c: any) => c.name).filter(Boolean)} + containers={(terminalPod?.containers || []).map((c: any) => c.name).filter(Boolean)} /> {/* ============ Pod 文件浏览器 ============ */} c.name).filter(Boolean)} + containers={(fileBrowserPod?.containers || []).map((c: any) => c.name).filter(Boolean)} /> {/* ============ Pod 详情弹窗 ============ */} - {podDetail && ( -
setPodDetail(null)}> -
e.stopPropagation()}> -
-

- - Pod 详情 -

- -
-
- {/* 基本信息 */} -
-

{i18n.t('k8s.editMode.basic')}

- {(() => { - 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 = { 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 ( -
-
- {i18n.t('kubernetes.secret.secretName')} -

{p.name || '-'}

-
-
- {i18n.t('kubernetes.namespace.title')} -

{p.namespace || '-'}

-
-
- {i18n.t('system.terminalStatus')} -
{st}
-
-
- {i18n.t('system.userCreatedAt')} -

{created ? new Date(created).toLocaleString('zh-CN') : '-'}

-
-
- {i18n.t('kubernetes.node.title')} -

{p.node_name || p.node || '-'}

-
-
- Pod IP -

{p.ip || p.pod_ip || '-'}

-
-
- {i18n.t('kubernetes.namespace.labels')} -
- {Object.keys(labels).length === 0 ? - - : Object.entries(labels).map(([k, v]) => ( - {k}: {String(v)} - )) - } -
-
-
- {i18n.t('kubernetes.namespace.annotations')} -
- {Object.keys(annotations).length === 0 ? - - : Object.entries(annotations).slice(0, 6).map(([k, v]) => ( - {k} - )) - } -
-
-
- ); - })()} -
- - {/* 现状详情(Conditions) */} -
-

{i18n.t('kubernetes.namespaceresour.s1d8d6e2e')}

- {(() => { - const cond: any[] = podDetail.conditions || podDetail.pod_conditions || []; - if (cond.length === 0) return

{i18n.t('aiops.assistantNoData')}

; - return ( - - - - {i18n.t('system.credentialType')} - {i18n.t('system.terminalStatus')} - {i18n.t('automation.scriptUpdatedAt')} - {i18n.t('kubernetes.event.eventReason')} - {i18n.t('kubernetes.event.eventMessage')} - - - - {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 ( - - {type} - - {st} - - {ts ? new Date(ts).toLocaleString('zh-CN') : '-'} - {reason} - {msg} - - ); - })} - -
- ); - })()} -
-
-
- -
-
-
- )} + setPodDetail(null)} /> ); } diff --git a/frontend/src/components/kubernetes/PodDetailDialog.tsx b/frontend/src/components/kubernetes/PodDetailDialog.tsx new file mode 100644 index 0000000..c59ae77 --- /dev/null +++ b/frontend/src/components/kubernetes/PodDetailDialog.tsx @@ -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 = { + 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 ( +
+
e.stopPropagation()}> +
+

+ + Pod 详情 +

+ +
+
+ {/* 基本信息 */} +
+

{i18n.t('k8s.editMode.basic')}

+
+
+ {i18n.t('kubernetes.secret.secretName')} +

{pod.name || '-'}

+
+
+ {i18n.t('kubernetes.namespace.title')} +

{pod.namespace || '-'}

+
+
+ {i18n.t('system.terminalStatus')} +
{st}
+
+
+ {i18n.t('system.userCreatedAt')} +

{created ? new Date(created).toLocaleString('zh-CN') : '-'}

+
+
+ {i18n.t('kubernetes.node.title')} +

{pod.node_name || pod.node || '-'}

+
+
+ Pod IP +

{pod.ip || pod.pod_ip || '-'}

+
+
+ {i18n.t('kubernetes.namespace.labels')} +
+ {Object.keys(labels).length === 0 ? - + : Object.entries(labels).map(([k, v]) => ( + {k}: {String(v)} + )) + } +
+
+
+ {i18n.t('kubernetes.namespace.annotations')} +
+ {Object.keys(annotations).length === 0 ? - + : Object.entries(annotations).slice(0, 6).map(([k, v]) => ( + {k} + )) + } +
+
+
+
+ + {/* 现状详情(Conditions) */} +
+

{i18n.t('kubernetes.namespaceresour.s1d8d6e2e')}

+ {cond.length === 0 ? ( +

{i18n.t('aiops.assistantNoData')}

+ ) : ( + + + + {i18n.t('system.credentialType')} + {i18n.t('system.terminalStatus')} + {i18n.t('automation.scriptUpdatedAt')} + {i18n.t('kubernetes.event.eventReason')} + {i18n.t('kubernetes.event.eventMessage')} + + + + {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 ( + + {ctype} + + {cst} + + {ts ? new Date(ts).toLocaleString('zh-CN') : '-'} + {reason} + {msg} + + ); + })} + +
+ )} +
+
+
+ +
+
+
+ ); +} diff --git a/frontend/src/components/kubernetes/namespace-resource-config.tsx b/frontend/src/components/kubernetes/namespace-resource-config.tsx new file mode 100644 index 0000000..1f70d73 --- /dev/null +++ b/frontend/src/components/kubernetes/namespace-resource-config.tsx @@ -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 } }) => ( +
+ + +
+); + +export const RESOURCE_CONFIG: Record; + 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) => ( + opts.onDetail?.(d)}> + {d.name} + {d.namespace} + + {(() => { + const ready = d.ready_replicas || 0; const total = d.replicas || 0; + const pct = total > 0 ? (ready / total) * 100 : 0; const done = ready === total; + return ( +
+
+
+
+ {ready}/{total} +
+ ); + })()} + + {d.image || '-'} + + ), + }, + 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 = { 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 ( + + opts.onPodDetail?.(p)}> +
+ + {p.name} +
+
+ {p.namespace} + {p.status || 'Unknown'} + {p.node_name || '-'} + {p.cpu_usage_m || '-'} + {p.mem_usage_mi || '-'} + 0 ? 'text-[var(--dg-orange)]' : 'text-[var(--dg-text-muted)]')}>{p.restart_count || 0} + {p.image || '-'} + +
+ + + + +
+
+
+ ); + }, + }, + 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) => ( + + {s.name} + {s.namespace} + {s.type || '-'} + {s.cluster_ip || '-'} + {s.ports || '-'} + e.stopPropagation()}> + + ), + }, + 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 ( + + {i.name} + {i.namespace} + {hosts.length > 0 ? hosts.join(', ') : '-'} + {i.tls ? 'HTTPS' : 'HTTP'} + e.stopPropagation()}> + + ); + }, + }, + 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) => ( + + {c.name} + {c.namespace} + {c.key_count || '-'} + e.stopPropagation()}> + + ), + }, + 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) => ( + + {s.name} + {s.namespace} + {s.type || '-'} + e.stopPropagation()}> + + ), + }, + 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) => ( + + {h.name} + {h.namespace} + {h.target_kind}/{h.target_name} + {h.min_replicas}/{h.max_replicas} + {h.cpu_percent ?? '-'}% + {h.mem_percent ?? '-'}% + + ), + }, + 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) => ( + + {cj.name} + {cj.namespace} + {cj.schedule || '-'} + {cj.suspend ? i18n.t('kubernetes.deployment.paused') : i18n.t('agent.agentServiceRunning')} + {cj.last_schedule || '-'} + + ), + }, + 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) => ( + + {p.name} + {p.namespace} + {p.phase || '-'} + {p.size || '-'} + {p.storage_class || '-'} + e.stopPropagation()}> + + ), + }, + 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) => ( + opts.onDetail?.(s)}> + {s.name} + {s.namespace} + {s.replicas || 0} + {s.ready || 0} + {s.image || '-'} + + ), + }, + 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) => ( + opts.onDetail?.(d)}> + {d.name} + {d.namespace} + {d.desired || 0} + {d.current || 0} + {d.ready || 0} + {d.image || '-'} + + ), + }, + 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) => ( + + {sa.name} + {sa.namespace} + {sa.secret_count ?? 0} + + {sa.image_pull_secrets && sa.image_pull_secrets.length > 0 ? sa.image_pull_secrets.join(', ') : '-'} + + {sa.age || '-'} + e.stopPropagation()}> + + + + ), + }, + 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 ( + + {r.name} + {r.namespace} + {r.rule_count ?? 0} + +
+ verbs: {verbsStr || '-'} + resources: {resStr || '-'} +
+
+ {r.age || '-'} + e.stopPropagation()}> +
+ + +
+
+
+ ); + }, + }, + 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 ( + + {rb.name} + {rb.namespace} + + + {rb.role_ref_kind || '-'}/{rb.role_ref_name || '-'} + + + + {subjStr || '-'} + + {rb.age || '-'} + e.stopPropagation()}> +
+ + +
+
+
+ ); + }, + }, +};