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:
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 },
|
||||
];
|
||||
}
|
||||
Reference in new issue
Block a user