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:
1 parent
b4591de97c
commit
e14b6d0e10
8 files changed
+441
-258
No files matched your search
@@ -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
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user