refactor(quality): Q28 前端巨型组件拆分 — KubernetesClusterDetail 547L→130L (-76%), 提取 cluster-detail/ 8 个文件 (types(computeClusterMetrics/buildTrendData 纯函数) + OverviewTab + NodesTab + PodsTab + DeploymentsTab + ServicesTab + IngressesTab + EventsTab), useMemo 聚合计算逻辑提取为模块级纯函数, demo 事件数据提取为模块级常量, 零逻辑变更, ESLint errors=0/warnings=65/any=0, tsc -b=0, npm build✓ (18.18s), go build=0, 17/17 测试通过

This commit is contained in:
attacker committed 2026-07-09 08:07:35 +08:00
1 parent 470832342e
commit 4c44fdde8b
11 files changed
+588 -444

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-Q27 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%) + DeploymentEditAdvanced 590L→289L (-51%) + WorkloadViewPage 572L→348L (-39%) + CloudAccountManager 572L→308L + ExcelServerImport 560L→385L + PipelineCreateDialog 559L→498L + PodLogsViewer 553L→331L (-40%) + **HostMonitor 547L→224L (-59%,提取 host-monitor/tabs/ 8 个文件 + TABS/PCT_REF_LINES 常量提取为模块级)**,累计提取 91 个独立组件文件** |
| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6-Q27 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%) + DeploymentEditAdvanced 590L→289L (-51%) + WorkloadViewPage 572L→348L (-39%) + CloudAccountManager 572L→308L + ExcelServerImport 560L→385L + PipelineCreateDialog 559L→498L + PodLogsViewer 553L→331L (-40%) + **HostMonitor 547L→224L (-59%,提取 host-monitor/tabs/ 8 个文件 + TABS/PCT_REF_LINES 常量提取为模块级)** + **KubernetesClusterDetail 547L→130L (-76%,提取 cluster-detail/ 8 个文件 + computeClusterMetrics/buildTrendData 纯函数提取)**,累计提取 99 个独立组件文件** |
### 3.2 🟡 中优先级
+41
View File
@@ -499,6 +499,47 @@ services/api.ts (1324L)
| Q25 | 前端巨型组件拆分(PipelineCreateDialog 559L→498L,提取 pipeline-create/types(PRESETS/DEFAULT_TARGET) + AdvancedSettingsPanel(172L 受控展开面板)) | P0 | **done** |
| Q26 | 前端巨型组件拆分(PodLogsViewer 553L→331L,提取 pod-logs-viewer/types(highlightKeyword 纯函数) + LogsHeader + ControlBar + LogContentArea + BottomStatusBar) | P0 | **done** |
| Q27 | 前端巨型组件拆分(HostMonitor 547L→224L (-59%),提取 host-monitor/tabs/ 8 个文件 (BasicTab + DiskTab + NetworkTab + PortMonitorTab + ProcessTable + FDUsageCard + ContextSwitchCard + ProcessTab),TABS 常量数组提取为模块级,PCT_REF_LINES 常量提升到 types.ts,端口/进程数组类型 JsonData→JsonData[] 类型安全升级) | P0 | **done** |
| Q28 | 前端巨型组件拆分(KubernetesClusterDetail 547L→130L (-76%),提取 cluster-detail/ 8 个文件 (types(computeClusterMetrics/buildTrendData 纯函数) + OverviewTab + NodesTab + PodsTab + DeploymentsTab + ServicesTab + IngressesTab + EventsTab)) | P0 | **done** |
---
## Q28: 前端巨型组件拆分 — KubernetesClusterDetail [done]
**问题**: `KubernetesClusterDetail.tsx`(547 行)是 Q27 拆分后项目最大的业务组件之一,包含集群详情页面的全部逻辑:5 个资源状态 useState、集群 metrics 聚合计算(computeClusterMetrics ~23 行)、趋势数据构造(buildTrendData ~13 行)、Promise.all 加载 5 类资源、以及 7 个 Tab 的全部内联 JSX(概览 Tab 含 Recharts AreaChart 资源趋势图 + 资源分布 4 卡片 ~100 行 / 节点表格 ~62 行 / Pod 表格 ~53 行 / Deployment 表格 ~42 行 / Service 表格 ~51 行 / Ingress 表格 ~47 行 / 事件 mock 列表 ~30 行)。单文件过大导致维护困难、代码导航低效。
**已完成 (2026-07-09)**:
将类型/纯函数和 7 个 Tab 内容区提取为 `cluster-detail/` 子目录下的独立文件:
| 新文件 | 行数 | 职责 |
|--------|------|------|
| `types.ts` | 60 | ClusterMetrics/TrendPoint 接口 + `computeClusterMetrics(nodes)` 纯函数(从节点 metrics 聚合集群级 CPU/内存使用率)+ `buildTrendData(metrics, t)` 纯函数(构造 5 点趋势数据避免单点光点效果) |
| `OverviewTab.tsx` | 129 | 概览 Tab(Recharts AreaChart CPU/内存双线趋势图 + 刷新按钮 + 资源分布 4 卡片网格 Pod/Service/Deployment/Node),受控展示组件接收 cluster + trendData + metrics + onRefresh |
| `NodesTab.tsx` | 69 | 节点表格(名称/角色徽章/状态徽章/CPU/内存/Pod 数/OS 7 列 + 空状态),受控展示组件接收 nodes |
| `PodsTab.tsx` | 60 | Pod 表格(名称/命名空间/状态徽章/重启次数/CPU/内存/节点 7 列 + 空状态),受控展示组件接收 pods |
| `DeploymentsTab.tsx` | 49 | Deployment 表格(名称/命名空间/副本数/可用副本/镜像/创建时间 6 列 + 空状态 + 可用率颜色),受控展示组件接收 deployments |
| `ServicesTab.tsx` | 58 | Service 表格(名称/命名空间/类型徽章/ClusterIP/端口/选择器 6 列 + 空状态),受控展示组件接收 services |
| `IngressesTab.tsx` | 54 | Ingress 表格(名称/命名空间/Hosts/Backend/TLS 徽章 5 列 + 空状态),受控展示组件接收 ingresses |
| `EventsTab.tsx` | 41 | 事件列表(5 条 demo 事件 + Normal/Warning 类型徽章 + 时间),纯展示组件无 props(事件数据提取为模块级 DEMO_EVENTS 常量) |
主文件 `KubernetesClusterDetail.tsx` 从 **547 行降至 130 行**(-76%),仅保留:5 个资源 useState、useMemo 调用纯函数(computeClusterMetrics + buildTrendData 一行替代 ~36 行计算逻辑)、loadClusterDetail 数据加载(Promise.all 5 资源 + toArray 归一化)、useEffect 触发、Card 外壳 + CardHeader(集群名/版本/节点数/创建时间 + 关闭按钮)、TabsList(7 TabsTrigger 含计数徽章)、以及 7 个子组件编排(OverviewTab + NodesTab + PodsTab + DeploymentsTab + ServicesTab + IngressesTab + EventsTab)。
**关键改进**:
- **纯函数提取**: 原 ~23 行 clusterMetrics 聚合计算逻辑(遍历节点、解析 cpu_usage_m/mem_usage_mi、计算百分比、加权平均)和 ~13 行 trendData 构造逻辑,分别提取为 `computeClusterMetrics(nodes)` 和 `buildTrendData(metrics, t)` 两个模块级纯函数。主组件的 useMemo 从 ~36 行降至 2 行调用。
- **Demo 事件数据提取**: 原 5 条内联事件对象数组(每次 render 重建 + 内联时间插值计算)提取为模块级 `DEMO_EVENTS` 常量 + `MIN_AGOS` 常量,消除重复创建。
**验收标准**:
- [x] KubernetesClusterDetail.tsx 从 547 行降至 **130 行**(-76%)
- [x] 8 个提取的文件全部 < 130 行(最大 OverviewTab 129 行,最小 EventsTab 41 行)
- [x] `tsc -b` 零错误
- [x] `npm run build` 零错误(✓ built in 18.18s)
- [x] ESLint errors 保持 **0**(KubernetesClusterDetail + cluster-detail/ 目录零 errors/warnings)
- [x] ESLint warnings 保持 **65**(无新增)
- [x] 全局 ESLint any 保持 **0**
- [x] `go build` 零错误
- [x] 17/17 集成回归测试通过(零回归)
- [x] Docker 部署 healthy,前端页面正常加载(HTTP 200)
- [x] 集群详情页功能不变(7 个 Tab:概览趋势图/节点/Pod/Deployment/Service/Ingress/事件全部正常)
---
@@ -2,24 +2,18 @@ import { useState, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Cloud, XCircle, RefreshCw } from 'lucide-react';
import { Cloud, XCircle } from 'lucide-react';
import { kubernetesApi } from '@/api';
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from 'recharts';
import type { JsonData } from '@/types/common';
// 资源使用趋势数据
// 由于当前无持久化时序存储,这里基于当前节点 metrics 构造一个简短的模拟趋势(最近几分钟变化)
// 真正历史数据需要后端记录指标快照后才能实现 // Logan.Li
import { computeClusterMetrics, buildTrendData } from './cluster-detail/types';
import { OverviewTab } from './cluster-detail/OverviewTab';
import { NodesTab } from './cluster-detail/NodesTab';
import { PodsTab } from './cluster-detail/PodsTab';
import { DeploymentsTab } from './cluster-detail/DeploymentsTab';
import { ServicesTab } from './cluster-detail/ServicesTab';
import { IngressesTab } from './cluster-detail/IngressesTab';
import { EventsTab } from './cluster-detail/EventsTab';
export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonData; onClose: () => void }) {
const { t } = useTranslation();
@@ -29,45 +23,11 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
const [services, setServices] = useState<JsonData[]>([]);
const [ingresses, setIngresses] = useState<JsonData[]>([]);
// 从节点数据计算集群级 CPU/内存使用率(实时快照) // Logan.Li
const clusterMetrics = useMemo(() => {
if (!nodes || nodes.length === 0) return { cpuPct: 0, memPct: 0 };
let totalCpuPct = 0, totalMemPct = 0, validNodes = 0;
for (const n of nodes) {
// 后端 ListNodes 返回 cpu_usage_m (如 "122m"), mem_usage_mi (如 "2105Mi"), cpu (核数), memory_mb (MB)
const cpuCores = n.cpu || 0;
const cpuUsageM = n.cpu_usage_m ? parseFloat(String(n.cpu_usage_m).replace('m', '')) : 0;
const cpuPct = cpuCores > 0 ? (cpuUsageM / 1000 / cpuCores) * 100 : 0;
const memMb = n.memory_mb || 0;
const memUsageMi = n.mem_usage_mi ? parseFloat(String(n.mem_usage_mi).replace('Mi', '').replace('Ki', '')) : 0;
const memUsageNorm = n.mem_usage_mi?.includes?.('Ki') ? memUsageMi / 1024 : n.mem_usage_mi?.includes?.('Gi') ? memUsageMi * 1024 : memUsageMi;
const memPct = memMb > 0 ? (memUsageNorm / memMb) * 100 : 0;
if (cpuPct > 0 || memPct > 0) {
totalCpuPct += cpuPct;
totalMemPct += memPct;
validNodes++;
}
}
return {
cpuPct: validNodes > 0 ? totalCpuPct / validNodes : 0,
memPct: validNodes > 0 ? totalMemPct / validNodes : 0,
};
}, [nodes]);
// 从节点数据计算集群级 CPU/内存使用率(实时快照)
const clusterMetrics = useMemo(() => computeClusterMetrics(nodes), [nodes]);
// 构造趋势数据(多点),避免单点导致“光点”效果 // Logan.Li
const trendData = useMemo(() => {
const cpu = Math.max(0, Math.min(100, Number(clusterMetrics.cpuPct.toFixed(1))));
const mem = Math.max(0, Math.min(100, Number(clusterMetrics.memPct.toFixed(1))));
// 模拟最近 5 个时间点的趋势(基于当前值轻微回溯)
return [
{ time: t('common.duration.min', { count: 10 }), cpu: Math.max(0, cpu * 0.72), memory: Math.max(0, mem * 0.78) },
{ time: t('common.duration.min', { count: 7 }), cpu: Math.max(0, cpu * 0.82), memory: Math.max(0, mem * 0.85) },
{ time: t('common.duration.min', { count: 4 }), cpu: Math.max(0, cpu * 0.90), memory: Math.max(0, mem * 0.93) },
{ time: t('common.duration.min', { count: 2 }), cpu: Math.max(0, cpu * 0.96), memory: Math.max(0, mem * 0.97) },
{ time: t('k8s.cluster.now'), cpu, memory: mem },
];
}, [clusterMetrics, t]);
// 构造趋势数据(多点),避免单点导致"光点"效果
const trendData = useMemo(() => buildTrendData(clusterMetrics, t), [clusterMetrics, t]);
const loadClusterDetail = async (clusterCode: string) => {
try {
@@ -149,397 +109,20 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="mt-4 space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card className="bg-[var(--dg-bg-tertiary)]/50 border-[var(--dg-border)]">
<CardHeader className="pb-2">
<div className="flex items-center justify-between w-full">
<CardTitle className="text-sm text-[var(--dg-text-secondary)]">{t('k8s.cluster.resourceTrend')}</CardTitle>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]"
onClick={() => cluster?.id && loadClusterDetail(cluster.id)}
title={t('k8s.cluster.refreshMetrics')}
>
<RefreshCw className="h-3.5 w-3.5" />
</Button>
</div>
</CardHeader>
<CardContent>
<div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={trendData} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
<defs>
<linearGradient id="colorCpu" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--dg-blue)" stopOpacity={0.35} />
<stop offset="95%" stopColor="var(--dg-blue)" stopOpacity={0.05} />
</linearGradient>
<linearGradient id="colorMemory" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--dg-green)" stopOpacity={0.35} />
<stop offset="95%" stopColor="var(--dg-green)" stopOpacity={0.05} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="2 2" stroke="var(--chart-grid)" />
<XAxis dataKey="time" stroke="var(--chart-axis)" fontSize={11} tickLine={false} />
<YAxis stroke="var(--chart-axis)" fontSize={11} domain={[0, 100]} unit="%" tickLine={false} />
<Tooltip
contentStyle={{
backgroundColor: 'var(--chart-tooltip-bg)',
border: '1px solid var(--chart-tooltip-border)',
borderRadius: '6px',
color: 'var(--dg-text-primary)',
fontSize: '12px',
}}
/>
<Area
type="monotone"
dataKey="cpu"
stroke="var(--dg-blue)"
strokeWidth={2}
fill="url(#colorCpu)"
dot={false}
activeDot={{ r: 3, fill: 'var(--dg-blue)' }}
name={t('k8s.label.cpuPercent')}
/>
<Area
type="monotone"
dataKey="memory"
stroke="var(--dg-green)"
strokeWidth={2}
fill="url(#colorMemory)"
dot={false}
activeDot={{ r: 3, fill: 'var(--dg-green)' }}
name={t('k8s.label.memoryPercent')}
/>
</AreaChart>
</ResponsiveContainer>
</div>
<div className="flex justify-center gap-6 mt-2 text-xs text-[var(--dg-text-secondary)]">
<span>{t('k8s.label.cpu')}: <span className="text-[var(--dg-blue)] font-bold">{clusterMetrics.cpuPct.toFixed(1)}%</span></span>
<span>{t('k8s.label.memory')}: <span className="text-[var(--dg-green)] font-bold">{clusterMetrics.memPct.toFixed(1)}%</span></span>
</div>
</CardContent>
</Card>
<Card className="bg-[var(--dg-bg-tertiary)]/50 border-[var(--dg-border)]">
<CardHeader className="pb-2">
<CardTitle className="text-sm text-[var(--dg-text-secondary)]">{t('k8s.cluster.resourceDistribution')}</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div className="text-center p-4 rounded-lg bg-[var(--dg-bg-tertiary)]">
<div className="text-3xl font-bold text-[var(--dg-blue)]">{cluster.pod_count || 0}</div>
<div className="text-sm text-[var(--dg-text-muted)] mt-1">{t('k8s.tab.pods')}</div>
</div>
<div className="text-center p-4 rounded-lg bg-[var(--dg-bg-tertiary)]">
<div className="text-3xl font-bold text-[var(--dg-green)]">{cluster.service_count || 0}</div>
<div className="text-sm text-[var(--dg-text-muted)] mt-1">{t('k8s.tab.services')}</div>
</div>
<div className="text-center p-4 rounded-lg bg-[var(--dg-bg-tertiary)]">
<div className="text-3xl font-bold text-[var(--dg-purple)]">{cluster.deployment_count || 0}</div>
<div className="text-sm text-[var(--dg-text-muted)] mt-1">{t('k8s.tab.deployments')}</div>
</div>
<div className="text-center p-4 rounded-lg bg-[var(--dg-bg-tertiary)]">
<div className="text-3xl font-bold text-[var(--dg-orange)]">{cluster.node_count || 0}</div>
<div className="text-sm text-[var(--dg-text-muted)] mt-1">{t('k8s.tab.nodes')}</div>
</div>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="nodes" className="mt-4">
<div className="border rounded-lg border-[var(--dg-border)] overflow-hidden">
<table className="w-full">
<thead className="bg-[var(--dg-bg-tertiary)]">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.name')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.role')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('common.status')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.cpu')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.memory')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.tab.pods')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.os')}</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--dg-border)]">
{nodes.length === 0 ? (
<tr>
<td colSpan={7} className="px-4 py-8 text-center text-[var(--dg-text-muted)]">{t('k8s.empty.nodes')}</td>
</tr>
) : (
nodes.map((node) => (
<tr key={node.id || node.name} className="hover:bg-[var(--dg-bg-tertiary)]/50">
<td className="px-4 py-3 text-sm text-[var(--dg-text-primary)]">{node.name}</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
node.role === 'master'
? 'border-[var(--dg-purple)]/30 text-[var(--dg-purple)] bg-[var(--dg-purple)]/10'
: 'border-[var(--dg-blue)]/30 text-[var(--dg-blue)] bg-[var(--dg-blue)]/10'
}
>
{node.role === 'master' ? 'Master' : 'Worker'}
</Badge>
</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
(node.status || '').toLowerCase() === 'ready'
? 'border-[var(--dg-green)]/30 text-[var(--dg-green)] bg-[var(--dg-green)]/10'
: 'border-[var(--dg-red)]/30 text-[var(--dg-red)] bg-[var(--dg-red)]/10'
}
>
{(node.status || '').toLowerCase() === 'ready' ? 'Ready' : 'NotReady'}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">
{node.cpu_usage_m || '0m'} / {node.cpu}{t('k8s.label.cores')}
</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">
{node.mem_usage_mi || '0Mi'} / {node.memory_mb ? (node.memory_mb / 1024).toFixed(0) + 'GB' : '-'}
</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{node.max_pods ?? 0}</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{node.os}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</TabsContent>
<TabsContent value="pods" className="mt-4">
<div className="border rounded-lg border-[var(--dg-border)] overflow-hidden">
<table className="w-full">
<thead className="bg-[var(--dg-bg-tertiary)]">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.name')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.namespace')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('common.status')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.restartCount')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.cpu')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.memory')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.node')}</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--dg-border)]">
{pods.length === 0 ? (
<tr>
<td colSpan={7} className="px-4 py-8 text-center text-[var(--dg-text-muted)]">{t('k8s.empty.pods')}</td>
</tr>
) : (
pods.map((pod) => (
<tr key={pod.id || pod.name} className="hover:bg-[var(--dg-bg-tertiary)]/50">
<td className="px-4 py-3 text-sm text-[var(--dg-text-primary)]">{pod.name}</td>
<td className="px-4 py-3">
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
{pod.namespace}
</Badge>
</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
(pod.status || '').toLowerCase() === 'running'
? 'border-[var(--dg-green)]/30 text-[var(--dg-green)] bg-[var(--dg-green)]/10'
: (pod.status || '').toLowerCase() === 'pending'
? 'border-[var(--dg-orange)]/30 text-[var(--dg-orange)] bg-[var(--dg-orange)]/10'
: 'border-[var(--dg-red)]/30 text-[var(--dg-red)] bg-[var(--dg-red)]/10'
}
>
{pod.status}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{pod.restart_count ?? 0}</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{pod.cpu_usage_m || '0m'}</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{pod.mem_usage_mi || '0Mi'}</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{pod.node_name}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</TabsContent>
<TabsContent value="deployments" className="mt-4">
<div className="border rounded-lg border-[var(--dg-border)] overflow-hidden">
<table className="w-full">
<thead className="bg-[var(--dg-bg-tertiary)]">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.name')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.namespace')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.replicas')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.availableReplicas')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.image')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.createdAt')}</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--dg-border)]">
{deployments.length === 0 ? (
<tr>
<td colSpan={6} className="px-4 py-8 text-center text-[var(--dg-text-muted)]">{t('k8s.empty.deployments')}</td>
</tr>
) : (
deployments.map((deployment) => (
<tr key={deployment.id || deployment.name} className="hover:bg-[var(--dg-bg-tertiary)]/50">
<td className="px-4 py-3 text-sm text-[var(--dg-text-primary)]">{deployment.name}</td>
<td className="px-4 py-3">
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
{deployment.namespace}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{deployment.replicas}</td>
<td className="px-4 py-3">
<span className={`text-sm ${(deployment.available_replicas ?? deployment.availableReplicas) === deployment.replicas ? 'text-[var(--dg-green)]' : 'text-[var(--dg-orange)]'}`}>
{deployment.available_replicas ?? deployment.availableReplicas} / {deployment.replicas}
</span>
</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)] truncate max-w-[200px]">{deployment.image}</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{deployment.created_at || deployment.createdAt}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</TabsContent>
<TabsContent value="services" className="mt-4">
<div className="border rounded-lg border-[var(--dg-border)] overflow-hidden">
<table className="w-full">
<thead className="bg-[var(--dg-bg-tertiary)]">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.name')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.namespace')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('common.type')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">ClusterIP</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.ports')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.selector')}</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--dg-border)]">
{services.length === 0 ? (
<tr>
<td colSpan={6} className="px-4 py-8 text-center text-[var(--dg-text-muted)]">{t('k8s.empty.services')}</td>
</tr>
) : (
services.map((service) => (
<tr key={service.id || service.name} className="hover:bg-[var(--dg-bg-tertiary)]/50">
<td className="px-4 py-3 text-sm text-[var(--dg-text-primary)]">{service.name}</td>
<td className="px-4 py-3">
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
{service.namespace}
</Badge>
</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
service.type === 'ClusterIP'
? 'border-[var(--dg-blue)]/30 text-[var(--dg-blue)] bg-[var(--dg-blue)]/10'
: service.type === 'NodePort'
? 'border-[var(--dg-purple)]/30 text-[var(--dg-purple)] bg-[var(--dg-purple)]/10'
: 'border-[var(--dg-green)]/30 text-[var(--dg-green)] bg-[var(--dg-green)]/10'
}
>
{service.type}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{service.cluster_ip ?? service.clusterIP}</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{service.ports}</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{service.selector}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</TabsContent>
<TabsContent value="ingresses" className="mt-4">
<div className="border rounded-lg border-[var(--dg-border)] overflow-hidden">
<table className="w-full">
<thead className="bg-[var(--dg-bg-tertiary)]">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.name')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.namespace')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.hosts')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.backend')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.tls')}</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--dg-border)]">
{ingresses.length === 0 ? (
<tr>
<td colSpan={5} className="px-4 py-8 text-center text-[var(--dg-text-muted)]">{t('k8s.empty.ingresses')}</td>
</tr>
) : (
ingresses.map((ingress) => (
<tr key={ingress.id || ingress.name} className="hover:bg-[var(--dg-bg-tertiary)]/50">
<td className="px-4 py-3 text-sm text-[var(--dg-text-primary)]">{ingress.name}</td>
<td className="px-4 py-3">
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
{ingress.namespace}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{ingress.hosts}</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{ingress.backend}</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
ingress.tls
? 'border-[var(--dg-green)]/30 text-[var(--dg-green)] bg-[var(--dg-green)]/10'
: 'border-[var(--dg-border)] text-[var(--dg-text-secondary)]'
}
>
{ingress.tls ? t('k8s.label.enabled') : t('k8s.label.disabled')}
</Badge>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</TabsContent>
<TabsContent value="events" className="mt-4">
<div className="space-y-2">
{[
{ type: 'Normal', reason: 'Started', message: 'Started container nginx', time: t('common.duration.minAgo', { count: 2 }) },
{ type: 'Normal', reason: 'Created', message: 'Created pod nginx-deployment-xxx', time: t('common.duration.minAgo', { count: 5 }) },
{ type: 'Warning', reason: 'FailedScheduling', message: '0/3 nodes are available', time: t('common.duration.minAgo', { count: 10 }) },
{ type: 'Normal', reason: 'Pulling', message: 'Pulling image "nginx:latest"', time: t('common.duration.minAgo', { count: 15 }) },
{ type: 'Normal', reason: 'Pulled', message: 'Successfully pulled image', time: t('common.duration.minAgo', { count: 12 }) },
].map((event, index) => (
<div
key={index}
className="flex items-center gap-3 p-3 rounded-lg bg-[var(--dg-bg-tertiary)]/50"
>
<Badge
variant="outline"
className={
event.type === 'Normal'
? 'border-[var(--dg-green)]/30 text-[var(--dg-green)] bg-[var(--dg-green)]/10'
: 'border-[var(--dg-orange)]/30 text-[var(--dg-orange)] bg-[var(--dg-orange)]/10'
}
>
{event.type}
</Badge>
<span className="text-sm font-medium text-[var(--dg-text-secondary)] min-w-[120px]">{event.reason}</span>
<span className="text-sm text-[var(--dg-text-secondary)] flex-1">{event.message}</span>
<span className="text-xs text-[var(--dg-text-muted)]">{event.time}</span>
</div>
))}
</div>
<TabsContent value="overview" className="mt-4">
<OverviewTab
cluster={cluster}
trendData={trendData}
metrics={clusterMetrics}
onRefresh={() => cluster?.id && loadClusterDetail(cluster.id)}
/>
</TabsContent>
<TabsContent value="nodes" className="mt-4"><NodesTab nodes={nodes} /></TabsContent>
<TabsContent value="pods" className="mt-4"><PodsTab pods={pods} /></TabsContent>
<TabsContent value="deployments" className="mt-4"><DeploymentsTab deployments={deployments} /></TabsContent>
<TabsContent value="services" className="mt-4"><ServicesTab services={services} /></TabsContent>
<TabsContent value="ingresses" className="mt-4"><IngressesTab ingresses={ingresses} /></TabsContent>
<TabsContent value="events" className="mt-4"><EventsTab /></TabsContent>
</Tabs>
</CardContent>
</Card>
@@ -0,0 +1,49 @@
import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge';
import type { JsonData } from '@/types/common';
export function DeploymentsTab({ deployments }: { deployments: JsonData[] }) {
const { t } = useTranslation();
return (
<div className="border rounded-lg border-[var(--dg-border)] overflow-hidden">
<table className="w-full">
<thead className="bg-[var(--dg-bg-tertiary)]">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.name')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.namespace')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.replicas')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.availableReplicas')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.image')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.createdAt')}</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--dg-border)]">
{deployments.length === 0 ? (
<tr>
<td colSpan={6} className="px-4 py-8 text-center text-[var(--dg-text-muted)]">{t('k8s.empty.deployments')}</td>
</tr>
) : (
deployments.map((deployment) => (
<tr key={deployment.id || deployment.name} className="hover:bg-[var(--dg-bg-tertiary)]/50">
<td className="px-4 py-3 text-sm text-[var(--dg-text-primary)]">{deployment.name}</td>
<td className="px-4 py-3">
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
{deployment.namespace}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{deployment.replicas}</td>
<td className="px-4 py-3">
<span className={`text-sm ${(deployment.available_replicas ?? deployment.availableReplicas) === deployment.replicas ? 'text-[var(--dg-green)]' : 'text-[var(--dg-orange)]'}`}>
{deployment.available_replicas ?? deployment.availableReplicas} / {deployment.replicas}
</span>
</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)] truncate max-w-[200px]">{deployment.image}</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{deployment.created_at || deployment.createdAt}</td>
</tr>
))
)}
</tbody>
</table>
</div>
);
}
@@ -0,0 +1,41 @@
import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge';
/** 集群事件 mock 数据 — 真正的事件需后端 events/summary 端点对接 */
const DEMO_EVENTS = [
{ type: 'Normal', reason: 'Started', message: 'Started container nginx' },
{ type: 'Normal', reason: 'Created', message: 'Created pod nginx-deployment-xxx' },
{ type: 'Warning', reason: 'FailedScheduling', message: '0/3 nodes are available' },
{ type: 'Normal', reason: 'Pulling', message: 'Pulling image "nginx:latest"' },
{ type: 'Normal', reason: 'Pulled', message: 'Successfully pulled image' },
];
const MIN_AGOS = [2, 5, 10, 15, 12];
export function EventsTab() {
const { t } = useTranslation();
return (
<div className="space-y-2">
{DEMO_EVENTS.map((event, index) => (
<div
key={index}
className="flex items-center gap-3 p-3 rounded-lg bg-[var(--dg-bg-tertiary)]/50"
>
<Badge
variant="outline"
className={
event.type === 'Normal'
? 'border-[var(--dg-green)]/30 text-[var(--dg-green)] bg-[var(--dg-green)]/10'
: 'border-[var(--dg-orange)]/30 text-[var(--dg-orange)] bg-[var(--dg-orange)]/10'
}
>
{event.type}
</Badge>
<span className="text-sm font-medium text-[var(--dg-text-secondary)] min-w-[120px]">{event.reason}</span>
<span className="text-sm text-[var(--dg-text-secondary)] flex-1">{event.message}</span>
<span className="text-xs text-[var(--dg-text-muted)]">{t('common.duration.minAgo', { count: MIN_AGOS[index] })}</span>
</div>
))}
</div>
);
}
@@ -0,0 +1,54 @@
import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge';
import type { JsonData } from '@/types/common';
export function IngressesTab({ ingresses }: { ingresses: JsonData[] }) {
const { t } = useTranslation();
return (
<div className="border rounded-lg border-[var(--dg-border)] overflow-hidden">
<table className="w-full">
<thead className="bg-[var(--dg-bg-tertiary)]">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.name')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.namespace')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.hosts')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.backend')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.tls')}</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--dg-border)]">
{ingresses.length === 0 ? (
<tr>
<td colSpan={5} className="px-4 py-8 text-center text-[var(--dg-text-muted)]">{t('k8s.empty.ingresses')}</td>
</tr>
) : (
ingresses.map((ingress) => (
<tr key={ingress.id || ingress.name} className="hover:bg-[var(--dg-bg-tertiary)]/50">
<td className="px-4 py-3 text-sm text-[var(--dg-text-primary)]">{ingress.name}</td>
<td className="px-4 py-3">
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
{ingress.namespace}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{ingress.hosts}</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{ingress.backend}</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
ingress.tls
? 'border-[var(--dg-green)]/30 text-[var(--dg-green)] bg-[var(--dg-green)]/10'
: 'border-[var(--dg-border)] text-[var(--dg-text-secondary)]'
}
>
{ingress.tls ? t('k8s.label.enabled') : t('k8s.label.disabled')}
</Badge>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
);
}
@@ -0,0 +1,69 @@
import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge';
import type { JsonData } from '@/types/common';
export function NodesTab({ nodes }: { nodes: JsonData[] }) {
const { t } = useTranslation();
return (
<div className="border rounded-lg border-[var(--dg-border)] overflow-hidden">
<table className="w-full">
<thead className="bg-[var(--dg-bg-tertiary)]">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.name')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.role')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('common.status')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.cpu')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.memory')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.tab.pods')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.os')}</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--dg-border)]">
{nodes.length === 0 ? (
<tr>
<td colSpan={7} className="px-4 py-8 text-center text-[var(--dg-text-muted)]">{t('k8s.empty.nodes')}</td>
</tr>
) : (
nodes.map((node) => (
<tr key={node.id || node.name} className="hover:bg-[var(--dg-bg-tertiary)]/50">
<td className="px-4 py-3 text-sm text-[var(--dg-text-primary)]">{node.name}</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
node.role === 'master'
? 'border-[var(--dg-purple)]/30 text-[var(--dg-purple)] bg-[var(--dg-purple)]/10'
: 'border-[var(--dg-blue)]/30 text-[var(--dg-blue)] bg-[var(--dg-blue)]/10'
}
>
{node.role === 'master' ? 'Master' : 'Worker'}
</Badge>
</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
(node.status || '').toLowerCase() === 'ready'
? 'border-[var(--dg-green)]/30 text-[var(--dg-green)] bg-[var(--dg-green)]/10'
: 'border-[var(--dg-red)]/30 text-[var(--dg-red)] bg-[var(--dg-red)]/10'
}
>
{(node.status || '').toLowerCase() === 'ready' ? 'Ready' : 'NotReady'}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">
{node.cpu_usage_m || '0m'} / {node.cpu}{t('k8s.label.cores')}
</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">
{node.mem_usage_mi || '0Mi'} / {node.memory_mb ? (node.memory_mb / 1024).toFixed(0) + 'GB' : '-'}
</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{node.max_pods ?? 0}</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{node.os}</td>
</tr>
))
)}
</tbody>
</table>
</div>
);
}
@@ -0,0 +1,129 @@
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { RefreshCw } from 'lucide-react';
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from 'recharts';
import type { JsonData } from '@/types/common';
import type { ClusterMetrics, TrendPoint } from './types';
export function OverviewTab({
cluster,
trendData,
metrics,
onRefresh,
}: {
cluster: JsonData;
trendData: TrendPoint[];
metrics: ClusterMetrics;
onRefresh: () => void;
}) {
const { t } = useTranslation();
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card className="bg-[var(--dg-bg-tertiary)]/50 border-[var(--dg-border)]">
<CardHeader className="pb-2">
<div className="flex items-center justify-between w-full">
<CardTitle className="text-sm text-[var(--dg-text-secondary)]">{t('k8s.cluster.resourceTrend')}</CardTitle>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]"
onClick={onRefresh}
title={t('k8s.cluster.refreshMetrics')}
>
<RefreshCw className="h-3.5 w-3.5" />
</Button>
</div>
</CardHeader>
<CardContent>
<div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={trendData} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
<defs>
<linearGradient id="colorCpu" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--dg-blue)" stopOpacity={0.35} />
<stop offset="95%" stopColor="var(--dg-blue)" stopOpacity={0.05} />
</linearGradient>
<linearGradient id="colorMemory" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--dg-green)" stopOpacity={0.35} />
<stop offset="95%" stopColor="var(--dg-green)" stopOpacity={0.05} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="2 2" stroke="var(--chart-grid)" />
<XAxis dataKey="time" stroke="var(--chart-axis)" fontSize={11} tickLine={false} />
<YAxis stroke="var(--chart-axis)" fontSize={11} domain={[0, 100]} unit="%" tickLine={false} />
<Tooltip
contentStyle={{
backgroundColor: 'var(--chart-tooltip-bg)',
border: '1px solid var(--chart-tooltip-border)',
borderRadius: '6px',
color: 'var(--dg-text-primary)',
fontSize: '12px',
}}
/>
<Area
type="monotone"
dataKey="cpu"
stroke="var(--dg-blue)"
strokeWidth={2}
fill="url(#colorCpu)"
dot={false}
activeDot={{ r: 3, fill: 'var(--dg-blue)' }}
name={t('k8s.label.cpuPercent')}
/>
<Area
type="monotone"
dataKey="memory"
stroke="var(--dg-green)"
strokeWidth={2}
fill="url(#colorMemory)"
dot={false}
activeDot={{ r: 3, fill: 'var(--dg-green)' }}
name={t('k8s.label.memoryPercent')}
/>
</AreaChart>
</ResponsiveContainer>
</div>
<div className="flex justify-center gap-6 mt-2 text-xs text-[var(--dg-text-secondary)]">
<span>{t('k8s.label.cpu')}: <span className="text-[var(--dg-blue)] font-bold">{metrics.cpuPct.toFixed(1)}%</span></span>
<span>{t('k8s.label.memory')}: <span className="text-[var(--dg-green)] font-bold">{metrics.memPct.toFixed(1)}%</span></span>
</div>
</CardContent>
</Card>
<Card className="bg-[var(--dg-bg-tertiary)]/50 border-[var(--dg-border)]">
<CardHeader className="pb-2">
<CardTitle className="text-sm text-[var(--dg-text-secondary)]">{t('k8s.cluster.resourceDistribution')}</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div className="text-center p-4 rounded-lg bg-[var(--dg-bg-tertiary)]">
<div className="text-3xl font-bold text-[var(--dg-blue)]">{cluster.pod_count || 0}</div>
<div className="text-sm text-[var(--dg-text-muted)] mt-1">{t('k8s.tab.pods')}</div>
</div>
<div className="text-center p-4 rounded-lg bg-[var(--dg-bg-tertiary)]">
<div className="text-3xl font-bold text-[var(--dg-green)]">{cluster.service_count || 0}</div>
<div className="text-sm text-[var(--dg-text-muted)] mt-1">{t('k8s.tab.services')}</div>
</div>
<div className="text-center p-4 rounded-lg bg-[var(--dg-bg-tertiary)]">
<div className="text-3xl font-bold text-[var(--dg-purple)]">{cluster.deployment_count || 0}</div>
<div className="text-sm text-[var(--dg-text-muted)] mt-1">{t('k8s.tab.deployments')}</div>
</div>
<div className="text-center p-4 rounded-lg bg-[var(--dg-bg-tertiary)]">
<div className="text-3xl font-bold text-[var(--dg-orange)]">{cluster.node_count || 0}</div>
<div className="text-sm text-[var(--dg-text-muted)] mt-1">{t('k8s.tab.nodes')}</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
}
@@ -0,0 +1,60 @@
import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge';
import type { JsonData } from '@/types/common';
export function PodsTab({ pods }: { pods: JsonData[] }) {
const { t } = useTranslation();
return (
<div className="border rounded-lg border-[var(--dg-border)] overflow-hidden">
<table className="w-full">
<thead className="bg-[var(--dg-bg-tertiary)]">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.name')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.namespace')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('common.status')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.restartCount')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.cpu')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.memory')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.node')}</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--dg-border)]">
{pods.length === 0 ? (
<tr>
<td colSpan={7} className="px-4 py-8 text-center text-[var(--dg-text-muted)]">{t('k8s.empty.pods')}</td>
</tr>
) : (
pods.map((pod) => (
<tr key={pod.id || pod.name} className="hover:bg-[var(--dg-bg-tertiary)]/50">
<td className="px-4 py-3 text-sm text-[var(--dg-text-primary)]">{pod.name}</td>
<td className="px-4 py-3">
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
{pod.namespace}
</Badge>
</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
(pod.status || '').toLowerCase() === 'running'
? 'border-[var(--dg-green)]/30 text-[var(--dg-green)] bg-[var(--dg-green)]/10'
: (pod.status || '').toLowerCase() === 'pending'
? 'border-[var(--dg-orange)]/30 text-[var(--dg-orange)] bg-[var(--dg-orange)]/10'
: 'border-[var(--dg-red)]/30 text-[var(--dg-red)] bg-[var(--dg-red)]/10'
}
>
{pod.status}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{pod.restart_count ?? 0}</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{pod.cpu_usage_m || '0m'}</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{pod.mem_usage_mi || '0Mi'}</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{pod.node_name}</td>
</tr>
))
)}
</tbody>
</table>
</div>
);
}
@@ -0,0 +1,58 @@
import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge';
import type { JsonData } from '@/types/common';
export function ServicesTab({ services }: { services: JsonData[] }) {
const { t } = useTranslation();
return (
<div className="border rounded-lg border-[var(--dg-border)] overflow-hidden">
<table className="w-full">
<thead className="bg-[var(--dg-bg-tertiary)]">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.name')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.namespace')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('common.type')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">ClusterIP</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.ports')}</th>
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('k8s.column.selector')}</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--dg-border)]">
{services.length === 0 ? (
<tr>
<td colSpan={6} className="px-4 py-8 text-center text-[var(--dg-text-muted)]">{t('k8s.empty.services')}</td>
</tr>
) : (
services.map((service) => (
<tr key={service.id || service.name} className="hover:bg-[var(--dg-bg-tertiary)]/50">
<td className="px-4 py-3 text-sm text-[var(--dg-text-primary)]">{service.name}</td>
<td className="px-4 py-3">
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
{service.namespace}
</Badge>
</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
service.type === 'ClusterIP'
? 'border-[var(--dg-blue)]/30 text-[var(--dg-blue)] bg-[var(--dg-blue)]/10'
: service.type === 'NodePort'
? 'border-[var(--dg-purple)]/30 text-[var(--dg-purple)] bg-[var(--dg-purple)]/10'
: 'border-[var(--dg-green)]/30 text-[var(--dg-green)] bg-[var(--dg-green)]/10'
}
>
{service.type}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{service.cluster_ip ?? service.clusterIP}</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{service.ports}</td>
<td className="px-4 py-3 text-sm text-[var(--dg-text-secondary)]">{service.selector}</td>
</tr>
))
)}
</tbody>
</table>
</div>
);
}
@@ -0,0 +1,60 @@
import type { JsonData } from '@/types/common';
import type { TFunction } from 'i18next';
/** 集群级 CPU/内存使用率快照(由节点 metrics 聚合计算) */
export interface ClusterMetrics {
cpuPct: number;
memPct: number;
}
/** 资源趋势图数据点 */
export interface TrendPoint {
time: string;
cpu: number;
memory: number;
}
/**
* 从节点数据计算集群级 CPU/内存使用率(实时快照)。
*
* 后端 ListNodes 返回 cpu_usage_m (如 "122m"), mem_usage_mi (如 "2105Mi"),
* cpu (核数), memory_mb (MB)。纯函数,零副作用。
*/
export function computeClusterMetrics(nodes: JsonData[] | undefined | null): ClusterMetrics {
if (!nodes || nodes.length === 0) return { cpuPct: 0, memPct: 0 };
let totalCpuPct = 0, totalMemPct = 0, validNodes = 0;
for (const n of nodes) {
const cpuCores = n.cpu || 0;
const cpuUsageM = n.cpu_usage_m ? parseFloat(String(n.cpu_usage_m).replace('m', '')) : 0;
const cpuPct = cpuCores > 0 ? (cpuUsageM / 1000 / cpuCores) * 100 : 0;
const memMb = n.memory_mb || 0;
const memUsageMi = n.mem_usage_mi ? parseFloat(String(n.mem_usage_mi).replace('Mi', '').replace('Ki', '')) : 0;
const memUsageNorm = n.mem_usage_mi?.includes?.('Ki') ? memUsageMi / 1024 : n.mem_usage_mi?.includes?.('Gi') ? memUsageMi * 1024 : memUsageMi;
const memPct = memMb > 0 ? (memUsageNorm / memMb) * 100 : 0;
if (cpuPct > 0 || memPct > 0) {
totalCpuPct += cpuPct;
totalMemPct += memPct;
validNodes++;
}
}
return {
cpuPct: validNodes > 0 ? totalCpuPct / validNodes : 0,
memPct: validNodes > 0 ? totalMemPct / validNodes : 0,
};
}
/**
* 基于当前集群 metrics 构造多点趋势数据,避免单点导致"光点"效果。
* 模拟最近 5 个时间点的趋势(基于当前值轻微回溯)。
*/
export function buildTrendData(metrics: ClusterMetrics, t: TFunction): TrendPoint[] {
const cpu = Math.max(0, Math.min(100, Number(metrics.cpuPct.toFixed(1))));
const mem = Math.max(0, Math.min(100, Number(metrics.memPct.toFixed(1))));
return [
{ time: t('common.duration.min', { count: 10 }), cpu: Math.max(0, cpu * 0.72), memory: Math.max(0, mem * 0.78) },
{ time: t('common.duration.min', { count: 7 }), cpu: Math.max(0, cpu * 0.82), memory: Math.max(0, mem * 0.85) },
{ time: t('common.duration.min', { count: 4 }), cpu: Math.max(0, cpu * 0.90), memory: Math.max(0, mem * 0.93) },
{ time: t('common.duration.min', { count: 2 }), cpu: Math.max(0, cpu * 0.96), memory: Math.max(0, mem * 0.97) },
{ time: t('k8s.cluster.now'), cpu, memory: mem },
];
}