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

@@ -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 },
];
}