feat(k8s): M1 SyncCluster 增加 metrics 采集

后端:
- SyncCluster 中调 GetNodeMetrics() 获取 metrics-server 数据
- 节点 INSERT 增加 cpu_usage/memory_usage 百分比字段
- 新增 parseCpuNanoToMilli / parseMemKiToMi 辅助函数
- 采集 Pod metrics(为 M2 准备)

前端:
- KubernetesClusterDetail 删除硬编码 resourceData mock
- 改为从节点 metrics 实时计算集群 CPU/内存使用率
- 图表 Y 轴固定 0-100 域,底部显示百分比数值

新增文档:
- ROADMAP.md / TDD_PLAN.md / PROJECT_EVALUATION.md
This commit is contained in:
attacker committed 2026-06-21 21:45:42 +08:00
1 parent 3b112ee813
commit b3d2011243
5 files changed
+289 -729

No files matched your search

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useMemo } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
@@ -15,16 +15,8 @@ import {
ResponsiveContainer,
} from 'recharts';
// 模拟资源使用趋势数据
const resourceData = [
{ time: '00:00', cpu: 45, memory: 52 },
{ time: '04:00', cpu: 38, memory: 48 },
{ time: '08:00', cpu: 62, memory: 68 },
{ time: '12:00', cpu: 78, memory: 82 },
{ time: '16:00', cpu: 72, memory: 76 },
{ time: '20:00', cpu: 58, memory: 64 },
{ time: '24:00', cpu: 48, memory: 56 },
];
// 资源使用趋势数据 — 从节点 metrics 实时计算(不再用硬编码 mock) // Logan.Li
// 由于当前无 time-series 存储,用当前快照展示一个合理的汇总值
export function KubernetesClusterDetail({ cluster, onClose }: { cluster: any; onClose: () => void }) {
const [nodes, setNodes] = useState<any[]>([]);
@@ -33,6 +25,32 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster: any; on
const [services, setServices] = useState<any[]>([]);
const [ingresses, setIngresses] = useState<any[]>([]);
// 从节点数据计算集群级 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) {
// ClusterOverviewPage 读的是 cpu_usage_m / mem_usage_mi(实时路径)
// ClusterDetail 的 nodes 从 getNodes API 获取,也有这些字段
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 : (n.cpu_usage || 0);
const memMb = n.memory_mb || n.memory || 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 : (n.memory_usage || 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]);
const loadClusterDetail = async (clusterId: string) => {
try {
// 后端 ListNodes/ListPods/etc 返回 data 直接为数组(非 {list,total})
@@ -120,7 +138,9 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster: any; on
<CardContent>
<div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={resourceData}>
<AreaChart data={[
{ time: 'CPU', cpu: Number(clusterMetrics.cpuPct.toFixed(1)), memory: Number(clusterMetrics.memPct.toFixed(1)) },
]}>
<defs>
<linearGradient id="colorCpu" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3} />
@@ -133,7 +153,7 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster: any; on
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis dataKey="time" stroke="#475569" fontSize={12} />
<YAxis stroke="#475569" fontSize={12} />
<YAxis stroke="#475569" fontSize={12} domain={[0, 100]} />
<Tooltip
contentStyle={{
backgroundColor: '#0f172a',
@@ -148,7 +168,7 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster: any; on
stroke="#3b82f6"
fillOpacity={1}
fill="url(#colorCpu)"
name="CPU"
name="CPU(%)"
/>
<Area
type="monotone"
@@ -156,11 +176,15 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster: any; on
stroke="#10b981"
fillOpacity={1}
fill="url(#colorMemory)"
name="内存"
name="内存(%)"
/>
</AreaChart>
</ResponsiveContainer>
</div>
<div className="flex justify-center gap-6 mt-2 text-xs text-slate-400">
<span>CPU: <span className="text-blue-400 font-bold">{clusterMetrics.cpuPct.toFixed(1)}%</span></span>
<span>内存: <span className="text-emerald-400 font-bold">{clusterMetrics.memPct.toFixed(1)}%</span></span>
</div>
</CardContent>
</Card>