diff --git a/PROJECT_EVALUATION.md b/PROJECT_EVALUATION.md index 36b66a3..1a22afa 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-Q21 done: 最大组件 ResourceVisualEditor 882L→282L (-68%) + AgentCLI 814L→521L (-36%) + SystemSettingsTab 754L→173L (-77%) + NamespaceResourcePage 719L→300L (-58%) + ApprovalWorkflowPage 705L→167L (-76%) + PipelineRunDetail 678L→273L (-60%) + CloudServerImport 699L→404L (-42%,any[]→具体接口) + UnifiedQueryPanel 682L→529L (-22%) + ServerManager 652L→442L (-32%) + PipelineEditor 676L→406L (-40%) + RdpDialog 642L→442L (-31%) + DeploymentEditService 627L→394L (-37%) + TraceManager 605L→331L (-45%,修复 SpanTreeNode 内联定义反模式) + GrafanaDashboard 624L→78L (-87%,DRY 合并 Zabbix/Prometheus Tab + 移除文件级 eslint-disable + 类型安全升级) + **TopologyView 596L→472L (-21%,提取 TopologyCanvas/NodeDetailDialog/EditNodeDialog,移除文件级 eslint-disable + any→ClusterInfo 具体接口)** + **DeploymentEditAdvanced 590L→289L (-51%,提取 types(parser+builder 纯函数) + UpdateStrategyCard/TolerationsCard/PodMiscCard/PodSecurityCard/HostSettingsCard 5 个 Card 组件)**,累计提取 73 个独立组件文件** | +| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6-Q22 done: 最大组件 ResourceVisualEditor 882L→282L (-68%) + AgentCLI 814L→521L (-36%) + SystemSettingsTab 754L→173L (-77%) + NamespaceResourcePage 719L→300L (-58%) + ApprovalWorkflowPage 705L→167L (-76%) + PipelineRunDetail 678L→273L (-60%) + CloudServerImport 699L→404L (-42%,any[]→具体接口) + UnifiedQueryPanel 682L→529L (-22%) + ServerManager 652L→442L (-32%) + PipelineEditor 676L→406L (-40%) + RdpDialog 642L→442L (-31%) + DeploymentEditService 627L→394L (-37%) + TraceManager 605L→331L (-45%,修复 SpanTreeNode 内联定义反模式) + GrafanaDashboard 624L→78L (-87%,DRY 合并 Zabbix/Prometheus Tab + 移除文件级 eslint-disable + 类型安全升级) + **TopologyView 596L→472L (-21%,提取 TopologyCanvas/NodeDetailDialog/EditNodeDialog,移除文件级 eslint-disable + any→ClusterInfo 具体接口)** + **DeploymentEditAdvanced 590L→289L (-51%,提取 types(parser+builder 纯函数) + UpdateStrategyCard/TolerationsCard/PodMiscCard/PodSecurityCard/HostSettingsCard 5 个 Card 组件)** + **WorkloadViewPage 572L→348L (-39%,提取 types(parseWorkloadStats 纯函数) + shared-components(StatCard/LabelBadges/ContainerInfo) + UpdateStrategySelectorCards + PodInstancesTable + ScaleDialog)**,累计提取 78 个独立组件文件** | ### 3.2 🟡 中优先级 diff --git a/ROADMAP.md b/ROADMAP.md index 8fd80c2..89d79d6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -492,7 +492,45 @@ services/api.ts (1324L) | Q18 | 前端巨型组件拆分(TraceManager 605L→331L,提取 types + trace-helpers + SpanTreeNode + TraceDetailDialog + SpanDetailDialog,修复 SpanTreeNode 内联定义反模式) | P0 | **done** | | Q19 | 前端巨型组件拆分(GrafanaDashboard 624L→78L,提取 types + datasource-toolbar + BatchDeployDialog + AgentTab + ExternalDatasourceTab,DRY 合并 Zabbix/Prometheus Tab,移除文件级 eslint-disable,类型安全升级 any→具体接口) | P0 | **done** | | Q20 | 前端巨型组件拆分(TopologyView 596L→472L,提取 types + TopologyCanvas + NodeDetailDialog + EditNodeDialog,移除文件级 eslint-disable,any→具体接口类型) | P0 | **done** | -| Q21 | 前端巨型组件拆分(DeploymentEditAdvanced 590L→289L,提取 types(parser+builder 纯函数) + UpdateStrategyCard + TolerationsCard + PodMiscCard + PodSecurityCard + HostSettingsCard) | P0 | **done** | +| Q21 | 前端巨型组件拆分(DeploymentEditAdvanced 590L→289L,提取 types(parser+builder 纯函数) + UpdateStrategyCard/TolerationsCard/PodMiscCard/PodSecurityCard/HostSettingsCard) | P0 | **done** | +| Q22 | 前端巨型组件拆分(WorkloadViewPage 572L→348L,提取 types(parseWorkloadStats 纯函数) + shared-components(StatCard/LabelBadges/ContainerInfo) + UpdateStrategySelectorCards + PodInstancesTable + ScaleDialog) | P0 | **done** | + +--- + +## Q22: 前端巨型组件拆分 — WorkloadViewPage [done] + +**问题**: `WorkloadViewPage.tsx`(572 行)是 Q21 拆分后项目最大的 K8s 业务组件之一(排除 UI 库 sidebar/data-table),包含 StatefulSet/DaemonSet 详情页面的全部逻辑:StatCard/LabelBadges/ContainerInfo 辅助组件、manifest 解析(spec/status/containers 等 ~40 行字段提取)、视图/YAML 双模式切换、StatefulSet/DaemonSet 状态卡片变体(6 个 StatCard x 2 变体)、更新策略卡片 + 选择器/标签卡片(85 行双列网格块)、VolumeClaimTemplates 表格、容器模板卡片、Pod 实例列表表格(50 行)、以及伸缩弹窗。单文件过大导致维护困难。 + +**已完成 (2026-07-08)**: + +将类型/解析纯函数、共享展示组件、更新策略+选择器卡片、Pod 实例表格、伸缩弹窗提取为 `workload-view/` 子目录下的独立文件: + +| 新文件 | 行数 | 职责 | +|--------|------|------| +| `types.ts` | 87 | WorkloadKind/ViewMode 类型 + WorkloadStats 接口 + `parseWorkloadStats(manifest, kind)` 纯函数(提取 replicas/readyReplicas/desiredScheduled/numberReady/updateStrategy/vcts/selectorLabels/labels/annotations/containers/initContainers 等 ~20 个字段) | +| `shared-components.tsx` | 66 | StatCard(统计卡片)+ LabelBadges(键值对标签徽章组)+ ContainerInfo(容器信息子组件:名称/镜像/资源限制/端口) | +| `UpdateStrategySelectorCards.tsx` | 101 | 更新策略卡片 + 选择器/标签卡片(双列网格 85 行 JSX 块),受控展示组件接收 WorkloadStats | +| `PodInstancesTable.tsx` | 74 | Pod 实例列表表格卡片(Pod 名称/状态/节点/IP/重启次数/操作按钮),受控组件接收 pods + onTerminal/onFileBrowser 回调 | +| `ScaleDialog.tsx` | 41 | 伸缩弹窗(StatefulSet 副本数调整,+/- 按钮控制),受控组件接收 scaleValue/actionLoading + 回调 | + +主文件 `WorkloadViewPage.tsx` 从 **572 行降至 348 行**(-39%),仅保留:状态管理(manifest/pods/loading/scaleModal/scaleValue/actionLoading/terminalPod/fileBrowserPod/yamlModal ~10 个 useState)、数据加载(loadData: Promise.all 拉 YAML + Pods + 过滤)、事件处理(handleModeChange/handleRestart/handleScale)、manifest 解析(调用 `parseWorkloadStats()` 一行替代 ~40 行字段提取)、modeTabs、PageShell 布局(actions + 状态徽章 + YAML/视图条件渲染 + StatusCards + UpdateStrategySelectorCards + VolumeClaimTemplates + ContainerTemplate + PodInstancesTable 编排)、以及 4 个弹窗组件挂载(ScaleDialog + YamlViewer + PodExecTerminal + PodFileBrowser)。 + +**关键改进**: +- **纯函数提取**: 原 ~40 行 manifest 字段提取逻辑(spec/status/containers/replicas/desiredScheduled/numberReady/updateStrategy/vcts/selectorLabels/labels/annotations 等分散的 `const x = spec.x ?? 0` 赋值)提取为 `parseWorkloadStats(manifest, kind)` 纯函数,主组件从 ~40 行降至 1 行调用 + 解构。 +- **类型安全**: `catch (e) { e?.message }` 改为 `catch (e) { e instanceof Error ? e.message : String(e) }` 类型安全访问。动态字段使用 `JsonData` 类型(`Record` 别名)兼容嵌套属性访问,避免 `Record` 属性访问报错。 + +**验收标准**: +- [x] WorkloadViewPage.tsx 从 572 行降至 **348 行**(-39%) +- [x] 5 个提取的文件全部 < 105 行(最大 UpdateStrategySelectorCards 101 行) +- [x] `tsc -b` 零错误 +- [x] `npm run build` 零错误(✓ built in 16.96s) +- [x] ESLint errors 保持 **0**(WorkloadViewPage + workload-view/ 目录零 errors/warnings) +- [x] ESLint warnings 保持 **65**(无新增) +- [x] 全局 ESLint any 保持 **0** +- [x] `go build` 零错误 +- [x] 17/17 集成回归测试通过(零回归) +- [x] Docker 部署 healthy,前端页面正常加载(HTTP 200) +- [x] StatefulSet/DaemonSet 详情页功能不变(状态卡片/更新策略/选择器标签/VolumeClaimTemplates/容器模板/Pod 实例列表/伸缩弹窗/重启/终端/文件浏览器全部正常) --- diff --git a/frontend/src/components/kubernetes/WorkloadViewPage.tsx b/frontend/src/components/kubernetes/WorkloadViewPage.tsx index 6da4a46..bb6b8f2 100644 --- a/frontend/src/components/kubernetes/WorkloadViewPage.tsx +++ b/frontend/src/components/kubernetes/WorkloadViewPage.tsx @@ -10,8 +10,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { - ArrowLeft, RotateCcw, FileCode, Activity, Container, Terminal, - HardDrive, Minus, Plus, Boxes, Tag, Settings, Database, + ArrowLeft, RotateCcw, FileCode, Container, Boxes, Database, } from 'lucide-react'; import { kubernetesApi } from '@/api'; import { useToast } from '@/hooks/use-toast'; @@ -19,40 +18,16 @@ import { useConfirm } from '@/components/ui/confirm-dialog'; import { YamlViewer } from './YamlViewer'; import { PodExecTerminal } from './PodExecTerminal'; import { PodFileBrowser } from './PodFileBrowser'; -import { formatK8sTime, statusColor, STATUS_COLORS } from './k8s-yaml-utils'; +import { formatK8sTime, STATUS_COLORS } from './k8s-yaml-utils'; import { PageShell } from '@/components/layout/PageShell'; import type { JsonData } from '@/types/common'; import i18n from '@/i18n'; -type WorkloadKind = 'StatefulSet' | 'DaemonSet'; -type ViewMode = 'view' | 'yaml'; - -function StatCard({ label, value, sub, color }: { label: string; value: React.ReactNode; sub?: string; color?: string }) { - return ( - - -

{label}

-

{value}

- {sub &&

{sub}

} -
-
- ); -} - -// Helper: render key-value pairs as badges -function LabelBadges({ labels }: { labels: Record | undefined }) { - const entries = Object.entries(labels || {}); - if (entries.length === 0) return 无; - return ( -
- {entries.map(([k, v]) => ( - - {k}={v} - - ))} -
- ); -} +import { type WorkloadKind, type ViewMode, parseWorkloadStats } from './workload-view/types'; +import { StatCard, ContainerInfo } from './workload-view/shared-components'; +import { UpdateStrategySelectorCards } from './workload-view/UpdateStrategySelectorCards'; +import { PodInstancesTable } from './workload-view/PodInstancesTable'; +import { ScaleDialog } from './workload-view/ScaleDialog'; export default function WorkloadViewPage() { const params = useParams<{ clusterCode: string; namespace: string; name: string; mode?: string }>(); @@ -135,7 +110,7 @@ export default function WorkloadViewPage() { toast({ title: i18n.t('kubernetes.deploymentdetai.s68a70009'), description: `${kind} 滚动重启已触发` }); setTimeout(loadData, 1000); } catch (e) { - toast({ title: i18n.t('kubernetes.deployment.restartFailed'), description: e?.message || String(e), variant: 'destructive' }); + toast({ title: i18n.t('kubernetes.deployment.restartFailed'), description: e instanceof Error ? e.message : String(e), variant: 'destructive' }); } finally { setActionLoading(false); } }; @@ -147,47 +122,18 @@ export default function WorkloadViewPage() { setScaleModal(false); setTimeout(loadData, 1000); } catch (e) { - toast({ title: i18n.t('kubernetes.workloadviewpag.s0b329fa8'), description: e?.message || String(e), variant: 'destructive' }); + toast({ title: i18n.t('kubernetes.workloadviewpag.s0b329fa8'), description: e instanceof Error ? e.message : String(e), variant: 'destructive' }); } finally { setActionLoading(false); } }; // Parse status from manifest - const spec = manifest?.spec || {}; - const status = manifest?.status || {}; - const containers: Record[] = spec?.template?.spec?.containers || []; - const initContainers: Record[] = spec?.template?.spec?.initContainers || []; - - // For StatefulSet - const replicas = spec.replicas ?? 1; - const readyReplicas = status.readyReplicas ?? 0; - const currentReplicas = status.currentReplicas ?? 0; - const updatedReplicas = status.updatedReplicas ?? 0; - - // For DaemonSet - const desiredScheduled = status.desiredNumberScheduled ?? 0; - const currentScheduled = status.currentNumberScheduled ?? 0; - const numberReady = status.numberReady ?? 0; - const numberAvailable = status.numberAvailable ?? 0; - const numberUpdated = status.updatedNumberScheduled ?? 0; - - const isSts = kind === 'StatefulSet'; - const isHealthy = isSts - ? (readyReplicas === replicas && replicas > 0) - : (numberReady === desiredScheduled && desiredScheduled > 0); - - // Update strategy - const updateStrategy = spec.updateStrategy || {}; - const strategyType = updateStrategy.type || (isSts ? 'RollingUpdate' : 'RollingUpdate'); - - // VolumeClaimTemplates (StatefulSet only) - const vcts: Record[] = spec.volumeClaimTemplates || []; - - // Selector - const selectorLabels = spec.selector?.matchLabels || {}; - - // Labels & Annotations - const labels = manifest?.metadata?.labels || {}; - const annotations = manifest?.metadata?.annotations || {}; + const stats = parseWorkloadStats(manifest, kind); + const { + isSts, isHealthy, + replicas, readyReplicas, currentReplicas, updatedReplicas, + desiredScheduled, currentScheduled, numberReady, numberAvailable, numberUpdated, + vcts, containers, initContainers, + } = stats; const modeTabs = (
@@ -255,7 +201,7 @@ export default function WorkloadViewPage() { - + {formatK8sTime(manifest?.metadata?.creationTimestamp)}} color="var(--dg-text-secondary)" /> @@ -271,92 +217,8 @@ export default function WorkloadViewPage() { )}
- {/* Update Strategy + Selector */} -
- - - - 更新策略 - - - -
- {i18n.t('kubernetes.workloadviewpag.s0511a271')} - {strategyType} -
- {strategyType === 'RollingUpdate' && isSts && updateStrategy.rollingUpdate && ( - <> -
- {i18n.t('kubernetes.workloadviewpag.s45cb9d97')} - {updateStrategy.rollingUpdate.partition ?? 0} -
- {updateStrategy.rollingUpdate.maxUnavailable && ( -
- {i18n.t('kubernetes.daemonset.maxUnavailable')} - {typeof updateStrategy.rollingUpdate.maxUnavailable === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxUnavailable) : updateStrategy.rollingUpdate.maxUnavailable} -
- )} - - )} - {strategyType === 'RollingUpdate' && !isSts && updateStrategy.rollingUpdate && ( - <> - {updateStrategy.rollingUpdate.maxUnavailable && ( -
- {i18n.t('kubernetes.daemonset.maxUnavailable')} - {typeof updateStrategy.rollingUpdate.maxUnavailable === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxUnavailable) : updateStrategy.rollingUpdate.maxUnavailable} -
- )} - {updateStrategy.rollingUpdate.maxSurge && ( -
- {i18n.t('kubernetes.workloadviewpag.sfed4ce42')} - {typeof updateStrategy.rollingUpdate.maxSurge === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxSurge) : updateStrategy.rollingUpdate.maxSurge} -
- )} - - )} - {isSts && ( -
- Pod 管理策略 - {spec.podManagementPolicy || 'OrderedReady'} -
- )} -
- {i18n.t('kubernetes.workloadviewpag.s450a4e05')} - {spec.revisionHistoryLimit ?? 10} -
-
-
- - - - - 选择器与标签 - - - -
-

Selector (matchLabels)

- -
-
-

Labels

- -
- {Object.keys(annotations).length > 0 && ( -
-

Annotations

-
- {Object.entries(annotations).slice(0, 8).map(([k, _v]) => ( - - {k} - - ))} -
-
- )} -
-
-
+ {/* Update Strategy + Selector & Labels */} + {/* VolumeClaimTemplates (StatefulSet only) */} {isSts && vcts.length > 0 && ( @@ -427,82 +289,24 @@ export default function WorkloadViewPage() { - {/* Pod list */} - - - - Pod 实例 ({pods.length}) - - - - - - - Pod 名称 - {i18n.t('system.terminalStatus')} - {i18n.t('kubernetes.pod.nodeName')} - Pod IP - {i18n.t('agent.agentServiceRestart')} - {i18n.t('system.auditAction')} - - - - {pods.length === 0 ? ( - {i18n.t('k8s.workloads.noPod')} - ) : pods.map((pod: JsonData) => ( - - {pod.name} - - - {pod.status || 'Unknown'} - - - {pod.node_name || pod.node || '-'} - {pod.ip || pod.pod_ip || '-'} - 0 ? 'text-[var(--dg-orange)]' : 'text-[var(--dg-text-muted)]'}>{pod.restart_count || 0} - -
- - -
-
-
- ))} -
-
-
-
+ {/* Pod instances */} + ))} {/* Scale modal */} {scaleModal && ( -
setScaleModal(false)}> -
e.stopPropagation()}> -

{i18n.t('kubernetes.workloadviewpag.s7f541ee2')}

-
- - {scaleValue} - -
-
- - -
-
-
+ setScaleModal(false)} + onScale={handleScale} + /> )} {/* YAML modal */} @@ -542,31 +346,3 @@ export default function WorkloadViewPage() { ); } - -// Container info sub-component -function ContainerInfo({ c }: { c: JsonData}) { - return ( -
-
-
- - {c.name} -
- {c.image} -
-
-
- CPU: {c.resources?.requests?.cpu || '-'}/{c.resources?.limits?.cpu || '-'} -
-
- 内存: {c.resources?.requests?.memory || '-'}/{c.resources?.limits?.memory || '-'} -
-
- 端口: - {(c.ports || []).map((p: JsonData) => p.containerPort || p.port).join(', ') || '-'} - -
-
-
- ); -} diff --git a/frontend/src/components/kubernetes/workload-view/PodInstancesTable.tsx b/frontend/src/components/kubernetes/workload-view/PodInstancesTable.tsx new file mode 100644 index 0000000..f821bc2 --- /dev/null +++ b/frontend/src/components/kubernetes/workload-view/PodInstancesTable.tsx @@ -0,0 +1,74 @@ +// WorkloadViewPage — Pod 实例列表表格卡片 +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { + Table, TableBody, TableCell, TableHead, TableHeader, TableRow, +} from '@/components/ui/table'; +import { Activity, Terminal, HardDrive } from 'lucide-react'; +import { statusColor } from '../k8s-yaml-utils'; +import type { JsonData } from '@/types/common'; +import i18n from '@/i18n'; + +export function PodInstancesTable({ + pods, + onTerminal, + onFileBrowser, +}: { + pods: JsonData[]; + onTerminal: (pod: JsonData) => void; + onFileBrowser: (pod: JsonData) => void; +}) { + return ( + + + + Pod 实例 ({pods.length}) + + + + + + + Pod 名称 + {i18n.t('system.terminalStatus')} + {i18n.t('kubernetes.pod.nodeName')} + Pod IP + {i18n.t('agent.agentServiceRestart')} + {i18n.t('system.auditAction')} + + + + {pods.length === 0 ? ( + {i18n.t('k8s.workloads.noPod')} + ) : pods.map((pod: JsonData) => ( + + {pod.name} + + + {pod.status || 'Unknown'} + + + {pod.node_name || pod.node || '-'} + {pod.ip || pod.pod_ip || '-'} + 0 ? 'text-[var(--dg-orange)]' : 'text-[var(--dg-text-muted)]'}>{pod.restart_count || 0} + +
+ + +
+
+
+ ))} +
+
+
+
+ ); +} diff --git a/frontend/src/components/kubernetes/workload-view/ScaleDialog.tsx b/frontend/src/components/kubernetes/workload-view/ScaleDialog.tsx new file mode 100644 index 0000000..96a9444 --- /dev/null +++ b/frontend/src/components/kubernetes/workload-view/ScaleDialog.tsx @@ -0,0 +1,41 @@ +// WorkloadViewPage — 伸缩弹窗(StatefulSet 副本数调整) +import { Button } from '@/components/ui/button'; +import { Minus, Plus } from 'lucide-react'; +import i18n from '@/i18n'; + +export function ScaleDialog({ + scaleValue, + actionLoading, + onScaleValueChange, + onClose, + onScale, +}: { + scaleValue: number; + actionLoading: boolean; + onScaleValueChange: (v: number) => void; + onClose: () => void; + onScale: () => void; +}) { + return ( +
+
e.stopPropagation()}> +

{i18n.t('kubernetes.workloadviewpag.s7f541ee2')}

+
+ + {scaleValue} + +
+
+ + +
+
+
+ ); +} diff --git a/frontend/src/components/kubernetes/workload-view/UpdateStrategySelectorCards.tsx b/frontend/src/components/kubernetes/workload-view/UpdateStrategySelectorCards.tsx new file mode 100644 index 0000000..d2b63cb --- /dev/null +++ b/frontend/src/components/kubernetes/workload-view/UpdateStrategySelectorCards.tsx @@ -0,0 +1,101 @@ +// WorkloadViewPage — 更新策略卡片 + 选择器/标签卡片(双列网格) +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Settings, Tag } from 'lucide-react'; +import { LabelBadges } from './shared-components'; +import type { WorkloadStats } from './types'; +import i18n from '@/i18n'; + +export function UpdateStrategySelectorCards({ stats }: { stats: WorkloadStats }) { + const { isSts, strategyType, updateStrategy, spec, selectorLabels, labels, annotations } = stats; + + return ( +
+ {/* Update Strategy */} + + + + 更新策略 + + + +
+ {i18n.t('kubernetes.workloadviewpag.s0511a271')} + {strategyType} +
+ {strategyType === 'RollingUpdate' && isSts && updateStrategy.rollingUpdate && ( + <> +
+ {i18n.t('kubernetes.workloadviewpag.s45cb9d97')} + {updateStrategy.rollingUpdate.partition ?? 0} +
+ {updateStrategy.rollingUpdate.maxUnavailable && ( +
+ {i18n.t('kubernetes.daemonset.maxUnavailable')} + {typeof updateStrategy.rollingUpdate.maxUnavailable === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxUnavailable) : updateStrategy.rollingUpdate.maxUnavailable} +
+ )} + + )} + {strategyType === 'RollingUpdate' && !isSts && updateStrategy.rollingUpdate && ( + <> + {updateStrategy.rollingUpdate.maxUnavailable && ( +
+ {i18n.t('kubernetes.daemonset.maxUnavailable')} + {typeof updateStrategy.rollingUpdate.maxUnavailable === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxUnavailable) : updateStrategy.rollingUpdate.maxUnavailable} +
+ )} + {updateStrategy.rollingUpdate.maxSurge && ( +
+ {i18n.t('kubernetes.workloadviewpag.sfed4ce42')} + {typeof updateStrategy.rollingUpdate.maxSurge === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxSurge) : updateStrategy.rollingUpdate.maxSurge} +
+ )} + + )} + {isSts && ( +
+ Pod 管理策略 + {spec.podManagementPolicy || 'OrderedReady'} +
+ )} +
+ {i18n.t('kubernetes.workloadviewpag.s450a4e05')} + {spec.revisionHistoryLimit ?? 10} +
+
+
+ + {/* Selector & Labels */} + + + + 选择器与标签 + + + +
+

Selector (matchLabels)

+ +
+
+

Labels

+ +
+ {Object.keys(annotations).length > 0 && ( +
+

Annotations

+
+ {Object.entries(annotations).slice(0, 8).map(([k]) => ( + + {k} + + ))} +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/src/components/kubernetes/workload-view/shared-components.tsx b/frontend/src/components/kubernetes/workload-view/shared-components.tsx new file mode 100644 index 0000000..ed5738d --- /dev/null +++ b/frontend/src/components/kubernetes/workload-view/shared-components.tsx @@ -0,0 +1,66 @@ +// WorkloadViewPage 共享展示组件:StatCard + LabelBadges + ContainerInfo +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent } from '@/components/ui/card'; +import { Container } from 'lucide-react'; +import type { JsonData } from '@/types/common'; + +/** 统计卡片 */ +export function StatCard({ label, value, sub, color }: { + label: string; + value: React.ReactNode; + sub?: string; + color?: string; +}) { + return ( + + +

{label}

+

{value}

+ {sub &&

{sub}

} +
+
+ ); +} + +/** 键值对标签徽章组 */ +export function LabelBadges({ labels }: { labels: Record | undefined }) { + const entries = Object.entries(labels || {}); + if (entries.length === 0) return 无; + return ( +
+ {entries.map(([k, v]) => ( + + {k}={v} + + ))} +
+ ); +} + +/** 容器信息子组件(名称/镜像/资源限制/端口) */ +export function ContainerInfo({ c }: { c: JsonData }) { + return ( +
+
+
+ + {c.name} +
+ {c.image} +
+
+
+ CPU: {c.resources?.requests?.cpu || '-'}/{c.resources?.limits?.cpu || '-'} +
+
+ 内存: {c.resources?.requests?.memory || '-'}/{c.resources?.limits?.memory || '-'} +
+
+ 端口: + {(c.ports || []).map((p: JsonData) => p.containerPort || p.port).join(', ') || '-'} + +
+
+
+ ); +} diff --git a/frontend/src/components/kubernetes/workload-view/types.ts b/frontend/src/components/kubernetes/workload-view/types.ts new file mode 100644 index 0000000..bc4f6b4 --- /dev/null +++ b/frontend/src/components/kubernetes/workload-view/types.ts @@ -0,0 +1,87 @@ +// WorkloadViewPage 类型定义 + manifest 解析纯函数 +import type { JsonData } from '@/types/common'; + +export type WorkloadKind = 'StatefulSet' | 'DaemonSet'; +export type ViewMode = 'view' | 'yaml'; + +/** 从 manifest 中解析出的工作负载状态摘要 */ +export interface WorkloadStats { + isSts: boolean; + isHealthy: boolean; + // StatefulSet fields + replicas: number; + readyReplicas: number; + currentReplicas: number; + updatedReplicas: number; + // DaemonSet fields + desiredScheduled: number; + currentScheduled: number; + numberReady: number; + numberAvailable: number; + numberUpdated: number; + // Shared + strategyType: string; + updateStrategy: JsonData; + vcts: JsonData[]; + selectorLabels: Record; + labels: Record; + annotations: Record; + containers: JsonData[]; + initContainers: JsonData[]; + spec: JsonData; +} + +/** 纯函数:从 K8s manifest 中提取状态字段 */ +export function parseWorkloadStats( + manifest: JsonData, + kind: WorkloadKind, +): WorkloadStats { + const spec: JsonData = manifest?.spec || {}; + const status: JsonData = manifest?.status || {}; + + const containers: JsonData[] = spec?.template?.spec?.containers || []; + const initContainers: JsonData[] = spec?.template?.spec?.initContainers || []; + + const isSts = kind === 'StatefulSet'; + + const replicas = (spec.replicas as number) ?? 1; + const readyReplicas = (status.readyReplicas as number) ?? 0; + const currentReplicas = (status.currentReplicas as number) ?? 0; + const updatedReplicas = (status.updatedReplicas as number) ?? 0; + + const desiredScheduled = (status.desiredNumberScheduled as number) ?? 0; + const currentScheduled = (status.currentNumberScheduled as number) ?? 0; + const numberReady = (status.numberReady as number) ?? 0; + const numberAvailable = (status.numberAvailable as number) ?? 0; + const numberUpdated = (status.updatedNumberScheduled as number) ?? 0; + + const isHealthy = isSts + ? readyReplicas === replicas && replicas > 0 + : numberReady === desiredScheduled && desiredScheduled > 0; + + const updateStrategy: JsonData = spec.updateStrategy || {}; + const strategyType = (updateStrategy.type as string) || 'RollingUpdate'; + + return { + isSts, + isHealthy, + replicas, + readyReplicas, + currentReplicas, + updatedReplicas, + desiredScheduled, + currentScheduled, + numberReady, + numberAvailable, + numberUpdated, + strategyType, + updateStrategy, + vcts: (spec.volumeClaimTemplates as JsonData[]) || [], + selectorLabels: (spec.selector?.matchLabels as Record) || {}, + labels: (manifest?.metadata?.labels as Record) || {}, + annotations: (manifest?.metadata?.annotations as Record) || {}, + containers, + initContainers, + spec, + }; +}