From 4c44fdde8bbfa4d3d8662d8c3b4acf43a28938c2 Mon Sep 17 00:00:00 2001 From: attacker Date: Thu, 9 Jul 2026 08:07:35 +0800 Subject: [PATCH] =?UTF-8?q?refactor(quality):=20Q28=20=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E5=B7=A8=E5=9E=8B=E7=BB=84=E4=BB=B6=E6=8B=86=E5=88=86=20?= =?UTF-8?q?=E2=80=94=20KubernetesClusterDetail=20547L=E2=86=92130L=20(-76%?= =?UTF-8?q?),=20=E6=8F=90=E5=8F=96=20cluster-detail/=208=20=E4=B8=AA?= =?UTF-8?q?=E6=96=87=E4=BB=B6=20(types(computeClusterMetrics/buildTrendDat?= =?UTF-8?q?a=20=E7=BA=AF=E5=87=BD=E6=95=B0)=20+=20OverviewTab=20+=20NodesT?= =?UTF-8?q?ab=20+=20PodsTab=20+=20DeploymentsTab=20+=20ServicesTab=20+=20I?= =?UTF-8?q?ngressesTab=20+=20EventsTab),=20useMemo=20=E8=81=9A=E5=90=88?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=E9=80=BB=E8=BE=91=E6=8F=90=E5=8F=96=E4=B8=BA?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E7=BA=A7=E7=BA=AF=E5=87=BD=E6=95=B0,=20demo?= =?UTF-8?q?=20=E4=BA=8B=E4=BB=B6=E6=95=B0=E6=8D=AE=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E4=B8=BA=E6=A8=A1=E5=9D=97=E7=BA=A7=E5=B8=B8=E9=87=8F,=20?= =?UTF-8?q?=E9=9B=B6=E9=80=BB=E8=BE=91=E5=8F=98=E6=9B=B4,=20ESLint=20error?= =?UTF-8?q?s=3D0/warnings=3D65/any=3D0,=20tsc=20-b=3D0,=20npm=20build?= =?UTF-8?q?=E2=9C=93=20(18.18s),=20go=20build=3D0,=2017/17=20=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E9=80=9A=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PROJECT_EVALUATION.md | 2 +- ROADMAP.md | 41 ++ .../kubernetes/KubernetesClusterDetail.tsx | 469 +----------------- .../cluster-detail/DeploymentsTab.tsx | 49 ++ .../kubernetes/cluster-detail/EventsTab.tsx | 41 ++ .../cluster-detail/IngressesTab.tsx | 54 ++ .../kubernetes/cluster-detail/NodesTab.tsx | 69 +++ .../kubernetes/cluster-detail/OverviewTab.tsx | 129 +++++ .../kubernetes/cluster-detail/PodsTab.tsx | 60 +++ .../kubernetes/cluster-detail/ServicesTab.tsx | 58 +++ .../kubernetes/cluster-detail/types.ts | 60 +++ 11 files changed, 588 insertions(+), 444 deletions(-) create mode 100644 frontend/src/components/kubernetes/cluster-detail/DeploymentsTab.tsx create mode 100644 frontend/src/components/kubernetes/cluster-detail/EventsTab.tsx create mode 100644 frontend/src/components/kubernetes/cluster-detail/IngressesTab.tsx create mode 100644 frontend/src/components/kubernetes/cluster-detail/NodesTab.tsx create mode 100644 frontend/src/components/kubernetes/cluster-detail/OverviewTab.tsx create mode 100644 frontend/src/components/kubernetes/cluster-detail/PodsTab.tsx create mode 100644 frontend/src/components/kubernetes/cluster-detail/ServicesTab.tsx create mode 100644 frontend/src/components/kubernetes/cluster-detail/types.ts diff --git a/PROJECT_EVALUATION.md b/PROJECT_EVALUATION.md index 9e96872..acda6f1 100644 --- a/PROJECT_EVALUATION.md +++ b/PROJECT_EVALUATION.md @@ -129,7 +129,7 @@ | ~~`api.ts` 1324 行/352 方法~~ → 已拆分为 12 个域文件 (api/kubernetes.ts, deploy.ts, monitor.ts 等) | ✅ 已解决 | 前端 | ~~P0~~ done | | 前端 **1026 处** `any` 类型 | 类型安全缺失,重构风险高 | 前端全局 | ~~P0~~ **done** (any 1076→40, unused 25→6, console 93→1, tsc --noEmit=0) | | ~~K8s 双路由体系(集群域+Kuboard 域)~~ → code 路由统一为主,ID 路由标记 deprecated 向后兼容 | ✅ 已解决 | 后端 | ~~P1~~ done | -| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6-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 🟡 中优先级 diff --git a/ROADMAP.md b/ROADMAP.md index c343930..296ab02 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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/事件全部正常) --- diff --git a/frontend/src/components/kubernetes/KubernetesClusterDetail.tsx b/frontend/src/components/kubernetes/KubernetesClusterDetail.tsx index e0443f7..8b1eb9f 100644 --- a/frontend/src/components/kubernetes/KubernetesClusterDetail.tsx +++ b/frontend/src/components/kubernetes/KubernetesClusterDetail.tsx @@ -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([]); const [ingresses, setIngresses] = useState([]); - // 从节点数据计算集群级 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 - -
- - -
- {t('k8s.cluster.resourceTrend')} - -
-
- -
- - - - - - - - - - - - - - - - - - - - -
-
- {t('k8s.label.cpu')}: {clusterMetrics.cpuPct.toFixed(1)}% - {t('k8s.label.memory')}: {clusterMetrics.memPct.toFixed(1)}% -
-
-
- - - - {t('k8s.cluster.resourceDistribution')} - - -
-
-
{cluster.pod_count || 0}
-
{t('k8s.tab.pods')}
-
-
-
{cluster.service_count || 0}
-
{t('k8s.tab.services')}
-
-
-
{cluster.deployment_count || 0}
-
{t('k8s.tab.deployments')}
-
-
-
{cluster.node_count || 0}
-
{t('k8s.tab.nodes')}
-
-
-
-
-
-
- - -
- - - - - - - - - - - - - - {nodes.length === 0 ? ( - - - - ) : ( - nodes.map((node) => ( - - - - - - - - - - )) - )} - -
{t('k8s.column.name')}{t('k8s.column.role')}{t('common.status')}{t('k8s.column.cpu')}{t('k8s.column.memory')}{t('k8s.tab.pods')}{t('k8s.column.os')}
{t('k8s.empty.nodes')}
{node.name} - - {node.role === 'master' ? 'Master' : 'Worker'} - - - - {(node.status || '').toLowerCase() === 'ready' ? 'Ready' : 'NotReady'} - - - {node.cpu_usage_m || '0m'} / {node.cpu}{t('k8s.label.cores')} - - {node.mem_usage_mi || '0Mi'} / {node.memory_mb ? (node.memory_mb / 1024).toFixed(0) + 'GB' : '-'} - {node.max_pods ?? 0}{node.os}
-
-
- - -
- - - - - - - - - - - - - - {pods.length === 0 ? ( - - - - ) : ( - pods.map((pod) => ( - - - - - - - - - - )) - )} - -
{t('k8s.column.name')}{t('k8s.column.namespace')}{t('common.status')}{t('k8s.column.restartCount')}{t('k8s.column.cpu')}{t('k8s.column.memory')}{t('k8s.column.node')}
{t('k8s.empty.pods')}
{pod.name} - - {pod.namespace} - - - - {pod.status} - - {pod.restart_count ?? 0}{pod.cpu_usage_m || '0m'}{pod.mem_usage_mi || '0Mi'}{pod.node_name}
-
-
- - -
- - - - - - - - - - - - - {deployments.length === 0 ? ( - - - - ) : ( - deployments.map((deployment) => ( - - - - - - - - - )) - )} - -
{t('k8s.column.name')}{t('k8s.column.namespace')}{t('k8s.column.replicas')}{t('k8s.column.availableReplicas')}{t('k8s.column.image')}{t('k8s.column.createdAt')}
{t('k8s.empty.deployments')}
{deployment.name} - - {deployment.namespace} - - {deployment.replicas} - - {deployment.available_replicas ?? deployment.availableReplicas} / {deployment.replicas} - - {deployment.image}{deployment.created_at || deployment.createdAt}
-
-
- - -
- - - - - - - - - - - - - {services.length === 0 ? ( - - - - ) : ( - services.map((service) => ( - - - - - - - - - )) - )} - -
{t('k8s.column.name')}{t('k8s.column.namespace')}{t('common.type')}ClusterIP{t('k8s.column.ports')}{t('k8s.column.selector')}
{t('k8s.empty.services')}
{service.name} - - {service.namespace} - - - - {service.type} - - {service.cluster_ip ?? service.clusterIP}{service.ports}{service.selector}
-
-
- - -
- - - - - - - - - - - - {ingresses.length === 0 ? ( - - - - ) : ( - ingresses.map((ingress) => ( - - - - - - - - )) - )} - -
{t('k8s.column.name')}{t('k8s.column.namespace')}{t('k8s.column.hosts')}{t('k8s.column.backend')}{t('k8s.column.tls')}
{t('k8s.empty.ingresses')}
{ingress.name} - - {ingress.namespace} - - {ingress.hosts}{ingress.backend} - - {ingress.tls ? t('k8s.label.enabled') : t('k8s.label.disabled')} - -
-
-
- - -
- {[ - { 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) => ( -
- - {event.type} - - {event.reason} - {event.message} - {event.time} -
- ))} -
+ + cluster?.id && loadClusterDetail(cluster.id)} + /> + + + + + + diff --git a/frontend/src/components/kubernetes/cluster-detail/DeploymentsTab.tsx b/frontend/src/components/kubernetes/cluster-detail/DeploymentsTab.tsx new file mode 100644 index 0000000..34574fb --- /dev/null +++ b/frontend/src/components/kubernetes/cluster-detail/DeploymentsTab.tsx @@ -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 ( +
+ + + + + + + + + + + + + {deployments.length === 0 ? ( + + + + ) : ( + deployments.map((deployment) => ( + + + + + + + + + )) + )} + +
{t('k8s.column.name')}{t('k8s.column.namespace')}{t('k8s.column.replicas')}{t('k8s.column.availableReplicas')}{t('k8s.column.image')}{t('k8s.column.createdAt')}
{t('k8s.empty.deployments')}
{deployment.name} + + {deployment.namespace} + + {deployment.replicas} + + {deployment.available_replicas ?? deployment.availableReplicas} / {deployment.replicas} + + {deployment.image}{deployment.created_at || deployment.createdAt}
+
+ ); +} diff --git a/frontend/src/components/kubernetes/cluster-detail/EventsTab.tsx b/frontend/src/components/kubernetes/cluster-detail/EventsTab.tsx new file mode 100644 index 0000000..d951963 --- /dev/null +++ b/frontend/src/components/kubernetes/cluster-detail/EventsTab.tsx @@ -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 ( +
+ {DEMO_EVENTS.map((event, index) => ( +
+ + {event.type} + + {event.reason} + {event.message} + {t('common.duration.minAgo', { count: MIN_AGOS[index] })} +
+ ))} +
+ ); +} diff --git a/frontend/src/components/kubernetes/cluster-detail/IngressesTab.tsx b/frontend/src/components/kubernetes/cluster-detail/IngressesTab.tsx new file mode 100644 index 0000000..d04959e --- /dev/null +++ b/frontend/src/components/kubernetes/cluster-detail/IngressesTab.tsx @@ -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 ( +
+ + + + + + + + + + + + {ingresses.length === 0 ? ( + + + + ) : ( + ingresses.map((ingress) => ( + + + + + + + + )) + )} + +
{t('k8s.column.name')}{t('k8s.column.namespace')}{t('k8s.column.hosts')}{t('k8s.column.backend')}{t('k8s.column.tls')}
{t('k8s.empty.ingresses')}
{ingress.name} + + {ingress.namespace} + + {ingress.hosts}{ingress.backend} + + {ingress.tls ? t('k8s.label.enabled') : t('k8s.label.disabled')} + +
+
+ ); +} diff --git a/frontend/src/components/kubernetes/cluster-detail/NodesTab.tsx b/frontend/src/components/kubernetes/cluster-detail/NodesTab.tsx new file mode 100644 index 0000000..61cc4f7 --- /dev/null +++ b/frontend/src/components/kubernetes/cluster-detail/NodesTab.tsx @@ -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 ( +
+ + + + + + + + + + + + + + {nodes.length === 0 ? ( + + + + ) : ( + nodes.map((node) => ( + + + + + + + + + + )) + )} + +
{t('k8s.column.name')}{t('k8s.column.role')}{t('common.status')}{t('k8s.column.cpu')}{t('k8s.column.memory')}{t('k8s.tab.pods')}{t('k8s.column.os')}
{t('k8s.empty.nodes')}
{node.name} + + {node.role === 'master' ? 'Master' : 'Worker'} + + + + {(node.status || '').toLowerCase() === 'ready' ? 'Ready' : 'NotReady'} + + + {node.cpu_usage_m || '0m'} / {node.cpu}{t('k8s.label.cores')} + + {node.mem_usage_mi || '0Mi'} / {node.memory_mb ? (node.memory_mb / 1024).toFixed(0) + 'GB' : '-'} + {node.max_pods ?? 0}{node.os}
+
+ ); +} diff --git a/frontend/src/components/kubernetes/cluster-detail/OverviewTab.tsx b/frontend/src/components/kubernetes/cluster-detail/OverviewTab.tsx new file mode 100644 index 0000000..9ff5e1e --- /dev/null +++ b/frontend/src/components/kubernetes/cluster-detail/OverviewTab.tsx @@ -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 ( +
+ + +
+ {t('k8s.cluster.resourceTrend')} + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
+
+ {t('k8s.label.cpu')}: {metrics.cpuPct.toFixed(1)}% + {t('k8s.label.memory')}: {metrics.memPct.toFixed(1)}% +
+
+
+ + + + {t('k8s.cluster.resourceDistribution')} + + +
+
+
{cluster.pod_count || 0}
+
{t('k8s.tab.pods')}
+
+
+
{cluster.service_count || 0}
+
{t('k8s.tab.services')}
+
+
+
{cluster.deployment_count || 0}
+
{t('k8s.tab.deployments')}
+
+
+
{cluster.node_count || 0}
+
{t('k8s.tab.nodes')}
+
+
+
+
+
+ ); +} diff --git a/frontend/src/components/kubernetes/cluster-detail/PodsTab.tsx b/frontend/src/components/kubernetes/cluster-detail/PodsTab.tsx new file mode 100644 index 0000000..e19ac56 --- /dev/null +++ b/frontend/src/components/kubernetes/cluster-detail/PodsTab.tsx @@ -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 ( +
+ + + + + + + + + + + + + + {pods.length === 0 ? ( + + + + ) : ( + pods.map((pod) => ( + + + + + + + + + + )) + )} + +
{t('k8s.column.name')}{t('k8s.column.namespace')}{t('common.status')}{t('k8s.column.restartCount')}{t('k8s.column.cpu')}{t('k8s.column.memory')}{t('k8s.column.node')}
{t('k8s.empty.pods')}
{pod.name} + + {pod.namespace} + + + + {pod.status} + + {pod.restart_count ?? 0}{pod.cpu_usage_m || '0m'}{pod.mem_usage_mi || '0Mi'}{pod.node_name}
+
+ ); +} diff --git a/frontend/src/components/kubernetes/cluster-detail/ServicesTab.tsx b/frontend/src/components/kubernetes/cluster-detail/ServicesTab.tsx new file mode 100644 index 0000000..da69843 --- /dev/null +++ b/frontend/src/components/kubernetes/cluster-detail/ServicesTab.tsx @@ -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 ( +
+ + + + + + + + + + + + + {services.length === 0 ? ( + + + + ) : ( + services.map((service) => ( + + + + + + + + + )) + )} + +
{t('k8s.column.name')}{t('k8s.column.namespace')}{t('common.type')}ClusterIP{t('k8s.column.ports')}{t('k8s.column.selector')}
{t('k8s.empty.services')}
{service.name} + + {service.namespace} + + + + {service.type} + + {service.cluster_ip ?? service.clusterIP}{service.ports}{service.selector}
+
+ ); +} diff --git a/frontend/src/components/kubernetes/cluster-detail/types.ts b/frontend/src/components/kubernetes/cluster-detail/types.ts new file mode 100644 index 0000000..bbc3434 --- /dev/null +++ b/frontend/src/components/kubernetes/cluster-detail/types.ts @@ -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 }, + ]; +}