refactor(quality): Q22 前端巨型组件拆分 — WorkloadViewPage 572L→348L (-39%), 提取 workload-view/ 5 个独立文件 (types(parseWorkloadStats 纯函数) + shared-components(StatCard/LabelBadges/ContainerInfo) + UpdateStrategySelectorCards + PodInstancesTable + ScaleDialog), ~40 行 manifest 字段提取为纯函数, catch 类型安全升级, 零逻辑变更, ESLint errors=0/warnings=65/any=0, tsc -b=0, npm build✓, 17/17 测试通过

This commit is contained in:
attacker committed 2026-07-08 22:59:58 +08:00
1 parent b4591de97c
commit e14b6d0e10
8 files changed
+441 -258

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 |
| 前端 **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 🟡 中优先级
+39 -1
View File
@@ -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<string, any>` 别名)兼容嵌套属性访问,避免 `Record<string, unknown>` 属性访问报错。
**验收标准**:
- [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 实例列表/伸缩弹窗/重启/终端/文件浏览器全部正常)
---
@@ -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 (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-3">
<p className="text-xs text-[var(--dg-text-muted)]">{label}</p>
<p className="text-base font-bold mt-0.5" style={{ color: color || 'var(--dg-text-primary)' }}>{value}</p>
{sub && <p className="text-[10px] text-[var(--dg-text-muted)] mt-0.5">{sub}</p>}
</CardContent>
</Card>
);
}
// Helper: render key-value pairs as badges
function LabelBadges({ labels }: { labels: Record<string, string> | undefined }) {
const entries = Object.entries(labels || {});
if (entries.length === 0) return <span className="text-xs text-[var(--dg-text-muted)]">无</span>;
return (
<div className="flex flex-wrap gap-1.5">
{entries.map(([k, v]) => (
<Badge key={k} variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] text-[10px] font-mono">
{k}={v}
</Badge>
))}
</div>
);
}
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<string, unknown>[] = spec?.template?.spec?.containers || [];
const initContainers: Record<string, unknown>[] = 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<string, unknown>[] = 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 = (
<div className="flex items-center gap-1 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-lg p-0.5">
@@ -255,7 +201,7 @@ export default function WorkloadViewPage() {
<StatCard label={i18n.t('kubernetes.workloadviewpag.sea639697')} value={`${readyReplicas}/${replicas}`} color={isHealthy ? STATUS_COLORS.green : STATUS_COLORS.orange} sub={isHealthy ? i18n.t('kubernetes.workloadviewpag.s23c944c1') : i18n.t('kubernetes.workloadviewpag.sac100213')} />
<StatCard label={i18n.t('kubernetes.hpa.currentReplicas')} value={currentReplicas} color={STATUS_COLORS.blue} />
<StatCard label={i18n.t('kubernetes.deployment.updatedReplicas')} value={updatedReplicas} color={STATUS_COLORS.green} />
<StatCard label={i18n.t('kubernetes.workloadviewpag.sf592ef2f')} value={spec.serviceName || '-'} color="var(--dg-text-secondary)" />
<StatCard label={i18n.t('kubernetes.workloadviewpag.sf592ef2f')} value={stats.spec.serviceName || '-'} color="var(--dg-text-secondary)" />
<StatCard label={i18n.t('kubernetes.nodedetailpaget.s55edf286')} value={pods.length} color="var(--dg-text-secondary)" />
<StatCard label={i18n.t('system.userCreatedAt')} value={<span className="text-xs">{formatK8sTime(manifest?.metadata?.creationTimestamp)}</span>} color="var(--dg-text-secondary)" />
</>
@@ -271,92 +217,8 @@ export default function WorkloadViewPage() {
)}
</div>
{/* Update Strategy + Selector */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
<Settings className="h-4 w-4 text-[var(--dg-orange)]" />更新策略
</CardTitle>
</CardHeader>
<CardContent className="pt-0 space-y-2 text-xs">
<div className="flex justify-between">
<span className="text-[var(--dg-text-muted)]"> {i18n.t('kubernetes.workloadviewpag.s0511a271')} </span>
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">{strategyType}</Badge>
</div>
{strategyType === 'RollingUpdate' && isSts && updateStrategy.rollingUpdate && (
<>
<div className="flex justify-between">
<span className="text-[var(--dg-text-muted)]"> {i18n.t('kubernetes.workloadviewpag.s45cb9d97')} </span>
<span className="text-[var(--dg-text-secondary)] font-mono">{updateStrategy.rollingUpdate.partition ?? 0}</span>
</div>
{updateStrategy.rollingUpdate.maxUnavailable && (
<div className="flex justify-between">
<span className="text-[var(--dg-text-muted)]"> {i18n.t('kubernetes.daemonset.maxUnavailable')} </span>
<span className="text-[var(--dg-text-secondary)] font-mono">{typeof updateStrategy.rollingUpdate.maxUnavailable === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxUnavailable) : updateStrategy.rollingUpdate.maxUnavailable}</span>
</div>
)}
</>
)}
{strategyType === 'RollingUpdate' && !isSts && updateStrategy.rollingUpdate && (
<>
{updateStrategy.rollingUpdate.maxUnavailable && (
<div className="flex justify-between">
<span className="text-[var(--dg-text-muted)]"> {i18n.t('kubernetes.daemonset.maxUnavailable')} </span>
<span className="text-[var(--dg-text-secondary)] font-mono">{typeof updateStrategy.rollingUpdate.maxUnavailable === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxUnavailable) : updateStrategy.rollingUpdate.maxUnavailable}</span>
</div>
)}
{updateStrategy.rollingUpdate.maxSurge && (
<div className="flex justify-between">
<span className="text-[var(--dg-text-muted)]"> {i18n.t('kubernetes.workloadviewpag.sfed4ce42')} </span>
<span className="text-[var(--dg-text-secondary)] font-mono">{typeof updateStrategy.rollingUpdate.maxSurge === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxSurge) : updateStrategy.rollingUpdate.maxSurge}</span>
</div>
)}
</>
)}
{isSts && (
<div className="flex justify-between">
<span className="text-[var(--dg-text-muted)]">Pod 管理策略</span>
<span className="text-[var(--dg-text-secondary)] font-mono">{spec.podManagementPolicy || 'OrderedReady'}</span>
</div>
)}
<div className="flex justify-between">
<span className="text-[var(--dg-text-muted)]"> {i18n.t('kubernetes.workloadviewpag.s450a4e05')} </span>
<span className="text-[var(--dg-text-secondary)] font-mono">{spec.revisionHistoryLimit ?? 10}</span>
</div>
</CardContent>
</Card>
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
<Tag className="h-4 w-4 text-[var(--dg-purple)]" />选择器与标签
</CardTitle>
</CardHeader>
<CardContent className="pt-0 space-y-3 text-xs">
<div>
<p className="text-[var(--dg-text-muted)] mb-1.5">Selector (matchLabels)</p>
<LabelBadges labels={selectorLabels} />
</div>
<div>
<p className="text-[var(--dg-text-muted)] mb-1.5">Labels</p>
<LabelBadges labels={labels} />
</div>
{Object.keys(annotations).length > 0 && (
<div>
<p className="text-[var(--dg-text-muted)] mb-1.5">Annotations</p>
<div className="flex flex-wrap gap-1.5">
{Object.entries(annotations).slice(0, 8).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">
{k}
</Badge>
))}
</div>
</div>
)}
</CardContent>
</Card>
</div>
{/* Update Strategy + Selector & Labels */}
<UpdateStrategySelectorCards stats={stats} />
{/* VolumeClaimTemplates (StatefulSet only) */}
{isSts && vcts.length > 0 && (
@@ -427,82 +289,24 @@ export default function WorkloadViewPage() {
</CardContent>
</Card>
{/* Pod list */}
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
<Activity className="h-4 w-4 text-[var(--dg-green)]" />Pod 实例 ({pods.length})
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
<TableHead className="text-[var(--dg-text-secondary)]">Pod 名称</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]"> {i18n.t('system.terminalStatus')} </TableHead>
<TableHead className="text-[var(--dg-text-secondary)]"> {i18n.t('kubernetes.pod.nodeName')} </TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">Pod IP</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]"> {i18n.t('agent.agentServiceRestart')} </TableHead>
<TableHead className="text-[var(--dg-text-secondary)]"> {i18n.t('system.auditAction')} </TableHead>
</TableRow>
</TableHeader>
<TableBody>
{pods.length === 0 ? (
<TableRow><TableCell colSpan={6} className="text-center text-[var(--dg-text-muted)] py-8"> {i18n.t('k8s.workloads.noPod')} </TableCell></TableRow>
) : pods.map((pod: JsonData) => (
<TableRow key={pod.name} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/30">
<TableCell className="font-medium text-[var(--dg-text-primary)]">{pod.name}</TableCell>
<TableCell>
<Badge style={{ background: `${statusColor(pod.status)}22`, color: statusColor(pod.status), border: `1px solid ${statusColor(pod.status)}44` }}>
{pod.status || 'Unknown'}
</Badge>
</TableCell>
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{pod.node_name || pod.node || '-'}</TableCell>
<TableCell className="text-sm text-[var(--dg-text-secondary)] font-mono">{pod.ip || pod.pod_ip || '-'}</TableCell>
<TableCell className={(pod.restart_count || 0) > 0 ? 'text-[var(--dg-orange)]' : 'text-[var(--dg-text-muted)]'}>{pod.restart_count || 0}</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-[var(--dg-green)] hover:text-[var(--dg-green)] hover:bg-[var(--dg-green)]/10"
onClick={() => setTerminalPod(pod)}>
<Terminal className="h-3.5 w-3.5 mr-1" />终端
</Button>
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-[var(--dg-orange)] hover:text-[var(--dg-orange)] hover:bg-[var(--dg-orange)]/10"
onClick={() => setFileBrowserPod(pod)}>
<HardDrive className="h-3.5 w-3.5 mr-1" />文件浏览器
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
{/* Pod instances */}
<PodInstancesTable
pods={pods}
onTerminal={setTerminalPod}
onFileBrowser={setFileBrowserPod}
/>
</>
))}
{/* Scale modal */}
{scaleModal && (
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center" onClick={() => setScaleModal(false)}>
<div className="bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-lg p-6 w-80" onClick={e => e.stopPropagation()}>
<h3 className="text-sm font-semibold text-[var(--dg-text-primary)] mb-4"> {i18n.t('kubernetes.workloadviewpag.s7f541ee2')} </h3>
<div className="flex items-center justify-center gap-4 mb-4">
<Button variant="outline" size="icon" className="h-9 w-9" onClick={() => setScaleValue(Math.max(0, scaleValue - 1))}>
<Minus className="h-4 w-4" />
</Button>
<span className="text-2xl font-bold text-[var(--dg-text-primary)] w-12 text-center">{scaleValue}</span>
<Button variant="outline" size="icon" className="h-9 w-9" onClick={() => setScaleValue(scaleValue + 1)}>
<Plus className="h-4 w-4" />
</Button>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" className="flex-1 border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={() => setScaleModal(false)}> {i18n.t('rbac.jobRole.cancel')} </Button>
<Button size="sm" className="flex-1 bg-[var(--dg-blue)] hover:opacity-90" disabled={actionLoading} onClick={handleScale}>
{actionLoading ? i18n.t('automation.systemTaskRunning') : i18n.t('monitor.alertEvents.acknowledge')}
</Button>
</div>
</div>
</div>
<ScaleDialog
scaleValue={scaleValue}
actionLoading={actionLoading}
onScaleValueChange={setScaleValue}
onClose={() => setScaleModal(false)}
onScale={handleScale}
/>
)}
{/* YAML modal */}
@@ -542,31 +346,3 @@ export default function WorkloadViewPage() {
</PageShell>
);
}
// Container info sub-component
function ContainerInfo({ c }: { c: JsonData}) {
return (
<div className="border border-[var(--dg-border)] rounded-lg p-3 bg-[var(--dg-bg-primary)]/30">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Container className="h-4 w-4 text-[var(--dg-blue)]" />
<span className="text-sm font-semibold text-[var(--dg-text-primary)]">{c.name}</span>
</div>
<code className="text-[11px] text-[var(--dg-text-secondary)] font-mono truncate max-w-[400px]">{c.image}</code>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 text-xs">
<div className="text-[var(--dg-text-secondary)]">
CPU: <span className="text-[var(--dg-text-primary)] font-mono">{c.resources?.requests?.cpu || '-'}/{c.resources?.limits?.cpu || '-'}</span>
</div>
<div className="text-[var(--dg-text-secondary)]">
内存: <span className="text-[var(--dg-text-primary)] font-mono">{c.resources?.requests?.memory || '-'}/{c.resources?.limits?.memory || '-'}</span>
</div>
<div className="text-[var(--dg-text-secondary)]">
端口: <span className="text-[var(--dg-text-primary)] font-mono">
{(c.ports || []).map((p: JsonData) => p.containerPort || p.port).join(', ') || '-'}
</span>
</div>
</div>
</div>
);
}
@@ -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 (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
<Activity className="h-4 w-4 text-[var(--dg-green)]" />Pod 实例 ({pods.length})
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
<TableHead className="text-[var(--dg-text-secondary)]">Pod 名称</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]"> {i18n.t('system.terminalStatus')} </TableHead>
<TableHead className="text-[var(--dg-text-secondary)]"> {i18n.t('kubernetes.pod.nodeName')} </TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">Pod IP</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]"> {i18n.t('agent.agentServiceRestart')} </TableHead>
<TableHead className="text-[var(--dg-text-secondary)]"> {i18n.t('system.auditAction')} </TableHead>
</TableRow>
</TableHeader>
<TableBody>
{pods.length === 0 ? (
<TableRow><TableCell colSpan={6} className="text-center text-[var(--dg-text-muted)] py-8"> {i18n.t('k8s.workloads.noPod')} </TableCell></TableRow>
) : pods.map((pod: JsonData) => (
<TableRow key={pod.name} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/30">
<TableCell className="font-medium text-[var(--dg-text-primary)]">{pod.name}</TableCell>
<TableCell>
<Badge style={{ background: `${statusColor(pod.status)}22`, color: statusColor(pod.status), border: `1px solid ${statusColor(pod.status)}44` }}>
{pod.status || 'Unknown'}
</Badge>
</TableCell>
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{pod.node_name || pod.node || '-'}</TableCell>
<TableCell className="text-sm text-[var(--dg-text-secondary)] font-mono">{pod.ip || pod.pod_ip || '-'}</TableCell>
<TableCell className={(pod.restart_count || 0) > 0 ? 'text-[var(--dg-orange)]' : 'text-[var(--dg-text-muted)]'}>{pod.restart_count || 0}</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-[var(--dg-green)] hover:text-[var(--dg-green)] hover:bg-[var(--dg-green)]/10"
onClick={() => onTerminal(pod)}>
<Terminal className="h-3.5 w-3.5 mr-1" />终端
</Button>
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-[var(--dg-orange)] hover:text-[var(--dg-orange)] hover:bg-[var(--dg-orange)]/10"
onClick={() => onFileBrowser(pod)}>
<HardDrive className="h-3.5 w-3.5 mr-1" />文件浏览器
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
);
}
@@ -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 (
<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 p-6 w-80" onClick={e => e.stopPropagation()}>
<h3 className="text-sm font-semibold text-[var(--dg-text-primary)] mb-4"> {i18n.t('kubernetes.workloadviewpag.s7f541ee2')} </h3>
<div className="flex items-center justify-center gap-4 mb-4">
<Button variant="outline" size="icon" className="h-9 w-9" onClick={() => onScaleValueChange(Math.max(0, scaleValue - 1))}>
<Minus className="h-4 w-4" />
</Button>
<span className="text-2xl font-bold text-[var(--dg-text-primary)] w-12 text-center">{scaleValue}</span>
<Button variant="outline" size="icon" className="h-9 w-9" onClick={() => onScaleValueChange(scaleValue + 1)}>
<Plus className="h-4 w-4" />
</Button>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" className="flex-1 border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={onClose}> {i18n.t('rbac.jobRole.cancel')} </Button>
<Button size="sm" className="flex-1 bg-[var(--dg-blue)] hover:opacity-90" disabled={actionLoading} onClick={onScale}>
{actionLoading ? i18n.t('automation.systemTaskRunning') : i18n.t('monitor.alertEvents.acknowledge')}
</Button>
</div>
</div>
</div>
);
}
@@ -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 (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Update Strategy */}
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
<Settings className="h-4 w-4 text-[var(--dg-orange)]" />更新策略
</CardTitle>
</CardHeader>
<CardContent className="pt-0 space-y-2 text-xs">
<div className="flex justify-between">
<span className="text-[var(--dg-text-muted)]"> {i18n.t('kubernetes.workloadviewpag.s0511a271')} </span>
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">{strategyType}</Badge>
</div>
{strategyType === 'RollingUpdate' && isSts && updateStrategy.rollingUpdate && (
<>
<div className="flex justify-between">
<span className="text-[var(--dg-text-muted)]"> {i18n.t('kubernetes.workloadviewpag.s45cb9d97')} </span>
<span className="text-[var(--dg-text-secondary)] font-mono">{updateStrategy.rollingUpdate.partition ?? 0}</span>
</div>
{updateStrategy.rollingUpdate.maxUnavailable && (
<div className="flex justify-between">
<span className="text-[var(--dg-text-muted)]"> {i18n.t('kubernetes.daemonset.maxUnavailable')} </span>
<span className="text-[var(--dg-text-secondary)] font-mono">{typeof updateStrategy.rollingUpdate.maxUnavailable === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxUnavailable) : updateStrategy.rollingUpdate.maxUnavailable}</span>
</div>
)}
</>
)}
{strategyType === 'RollingUpdate' && !isSts && updateStrategy.rollingUpdate && (
<>
{updateStrategy.rollingUpdate.maxUnavailable && (
<div className="flex justify-between">
<span className="text-[var(--dg-text-muted)]"> {i18n.t('kubernetes.daemonset.maxUnavailable')} </span>
<span className="text-[var(--dg-text-secondary)] font-mono">{typeof updateStrategy.rollingUpdate.maxUnavailable === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxUnavailable) : updateStrategy.rollingUpdate.maxUnavailable}</span>
</div>
)}
{updateStrategy.rollingUpdate.maxSurge && (
<div className="flex justify-between">
<span className="text-[var(--dg-text-muted)]"> {i18n.t('kubernetes.workloadviewpag.sfed4ce42')} </span>
<span className="text-[var(--dg-text-secondary)] font-mono">{typeof updateStrategy.rollingUpdate.maxSurge === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxSurge) : updateStrategy.rollingUpdate.maxSurge}</span>
</div>
)}
</>
)}
{isSts && (
<div className="flex justify-between">
<span className="text-[var(--dg-text-muted)]">Pod 管理策略</span>
<span className="text-[var(--dg-text-secondary)] font-mono">{spec.podManagementPolicy || 'OrderedReady'}</span>
</div>
)}
<div className="flex justify-between">
<span className="text-[var(--dg-text-muted)]"> {i18n.t('kubernetes.workloadviewpag.s450a4e05')} </span>
<span className="text-[var(--dg-text-secondary)] font-mono">{spec.revisionHistoryLimit ?? 10}</span>
</div>
</CardContent>
</Card>
{/* Selector & Labels */}
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
<Tag className="h-4 w-4 text-[var(--dg-purple)]" />选择器与标签
</CardTitle>
</CardHeader>
<CardContent className="pt-0 space-y-3 text-xs">
<div>
<p className="text-[var(--dg-text-muted)] mb-1.5">Selector (matchLabels)</p>
<LabelBadges labels={selectorLabels} />
</div>
<div>
<p className="text-[var(--dg-text-muted)] mb-1.5">Labels</p>
<LabelBadges labels={labels} />
</div>
{Object.keys(annotations).length > 0 && (
<div>
<p className="text-[var(--dg-text-muted)] mb-1.5">Annotations</p>
<div className="flex flex-wrap gap-1.5">
{Object.entries(annotations).slice(0, 8).map(([k]) => (
<Badge key={k} variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-muted)] text-[10px] font-mono max-w-[300px] truncate">
{k}
</Badge>
))}
</div>
</div>
)}
</CardContent>
</Card>
</div>
);
}
@@ -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 (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-3">
<p className="text-xs text-[var(--dg-text-muted)]">{label}</p>
<p className="text-base font-bold mt-0.5" style={{ color: color || 'var(--dg-text-primary)' }}>{value}</p>
{sub && <p className="text-[10px] text-[var(--dg-text-muted)] mt-0.5">{sub}</p>}
</CardContent>
</Card>
);
}
/** 键值对标签徽章组 */
export function LabelBadges({ labels }: { labels: Record<string, string> | undefined }) {
const entries = Object.entries(labels || {});
if (entries.length === 0) return <span className="text-xs text-[var(--dg-text-muted)]">无</span>;
return (
<div className="flex flex-wrap gap-1.5">
{entries.map(([k, v]) => (
<Badge key={k} variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] text-[10px] font-mono">
{k}={v}
</Badge>
))}
</div>
);
}
/** 容器信息子组件(名称/镜像/资源限制/端口) */
export function ContainerInfo({ c }: { c: JsonData }) {
return (
<div className="border border-[var(--dg-border)] rounded-lg p-3 bg-[var(--dg-bg-primary)]/30">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Container className="h-4 w-4 text-[var(--dg-blue)]" />
<span className="text-sm font-semibold text-[var(--dg-text-primary)]">{c.name}</span>
</div>
<code className="text-[11px] text-[var(--dg-text-secondary)] font-mono truncate max-w-[400px]">{c.image}</code>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 text-xs">
<div className="text-[var(--dg-text-secondary)]">
CPU: <span className="text-[var(--dg-text-primary)] font-mono">{c.resources?.requests?.cpu || '-'}/{c.resources?.limits?.cpu || '-'}</span>
</div>
<div className="text-[var(--dg-text-secondary)]">
内存: <span className="text-[var(--dg-text-primary)] font-mono">{c.resources?.requests?.memory || '-'}/{c.resources?.limits?.memory || '-'}</span>
</div>
<div className="text-[var(--dg-text-secondary)]">
端口: <span className="text-[var(--dg-text-primary)] font-mono">
{(c.ports || []).map((p: JsonData) => p.containerPort || p.port).join(', ') || '-'}
</span>
</div>
</div>
</div>
);
}
@@ -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<string, string>;
labels: Record<string, string>;
annotations: Record<string, string>;
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<string, string>) || {},
labels: (manifest?.metadata?.labels as Record<string, string>) || {},
annotations: (manifest?.metadata?.annotations as Record<string, string>) || {},
containers,
initContainers,
spec,
};
}