Files
aiops/frontend/src/components/kubernetes/KubernetesClusterDetail.tsx
T
attacker c68aef4444 feat(alert): A2 告警引擎分布式安全 — Redis 分布式锁(SetNX)
- Cache 接口新增 SetNX 方法(go-redis 原子 / redka check-then-set 单副本安全)
- AlertEngine 改造为支持分布式锁: tryAcquireLock/releaseLock/GetEngineStatus
- 多副本部署时仅 Leader 执行告警检查, 60s TTL 防死锁, fail-open 容错
- 新增 GET /alerts/engine/status 监控端点(is_leader/instance_id/lock_holder)
- 修复 GrafanaDashboard.tsx Panel.options 类型不匹配导致构建中断
- 同步未提交的前端类型安全迁移(140+ 文件)
- ROADMAP/TDD_PLAN/PROJECT_EVALUATION 三文档同步更新

验证: go build 零错误, Docker 部署 healthy, 引擎状态端点正常,
锁竞争测试通过(非 Leader 跳过检查), 5/5 核心 API 回归通过
2026-06-26 21:13:15 +08:00

514 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useMemo } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
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 实时计算(不再用硬编码 mock) // Logan.Li
// 由于当前无 time-series 存储,用当前快照展示一个合理的汇总值
export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonData; onClose: () => void }) {
const [nodes, setNodes] = useState<JsonData[]>([]);
const [pods, setPods] = useState<JsonData[]>([]);
const [deployments, setDeployments] = useState<JsonData[]>([]);
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]);
const loadClusterDetail = async (clusterId: string) => {
try {
// 后端 ListNodes/ListPods/etc 返回 data 直接为数组(非 {list,total})
const [nodesRes, podsRes, depRes, svcRes, ingRes] = await Promise.all([
kubernetesApi.getNodes(clusterId).catch(() => []),
kubernetesApi.getPods(clusterId).catch(() => []),
kubernetesApi.getDeployments(clusterId).catch(() => []),
kubernetesApi.getServices(clusterId).catch(() => []),
kubernetesApi.getIngresses(clusterId).catch(() => []),
]);
const toArray = (v: JsonData) => (Array.isArray(v) ? v : (v?.list || []));
setNodes(toArray(nodesRes));
setPods(toArray(podsRes));
setDeployments(toArray(depRes));
setServices(toArray(svcRes));
setIngresses(toArray(ingRes));
} catch (e) {
console.error('加载集群详情失败:', e);
}
};
useEffect(() => {
if (cluster?.id) {
loadClusterDetail(cluster.id);
}
}, [cluster?.id]);
return (
<Card className="bg-slate-900 border-slate-800">
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Cloud className="h-6 w-6 text-blue-400" />
<div>
<CardTitle className="text-slate-100">{cluster.name}</CardTitle>
<CardDescription className="text-slate-500">
{cluster.version} · {cluster.node_count || 0} 节点 · 创建于{' '}
{cluster.created_at ? new Date(cluster.created_at).toLocaleDateString() : '-'}
</CardDescription>
</div>
</div>
<Button
variant="ghost"
size="sm"
className="text-slate-400 hover:text-slate-100"
onClick={onClose}
>
<XCircle className="h-4 w-4 mr-2" />
关闭
</Button>
</div>
</CardHeader>
<CardContent>
<Tabs defaultValue="overview" className="w-full">
<TabsList className="bg-slate-800 border border-slate-700 flex-wrap h-auto">
<TabsTrigger value="overview" className="data-[state=active]:bg-slate-700">
概览
</TabsTrigger>
<TabsTrigger value="nodes" className="data-[state=active]:bg-slate-700">
节点 ({nodes.length})
</TabsTrigger>
<TabsTrigger value="pods" className="data-[state=active]:bg-slate-700">
Pod ({pods.length})
</TabsTrigger>
<TabsTrigger value="deployments" className="data-[state=active]:bg-slate-700">
Deployment ({deployments.length})
</TabsTrigger>
<TabsTrigger value="services" className="data-[state=active]:bg-slate-700">
Service ({services.length})
</TabsTrigger>
<TabsTrigger value="ingresses" className="data-[state=active]:bg-slate-700">
Ingress ({ingresses.length})
</TabsTrigger>
<TabsTrigger value="events" className="data-[state=active]:bg-slate-700">
事件
</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-slate-800/50 border-slate-700">
<CardHeader className="pb-2">
<CardTitle className="text-sm text-slate-400">资源使用趋势</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={[
{ time: 'CPU', cpu: Number(clusterMetrics.cpuPct.toFixed(1)), memory: Number(clusterMetrics.memPct.toFixed(1)) },
]}>
<defs>
<linearGradient id="colorCpu" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3} />
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
</linearGradient>
<linearGradient id="colorMemory" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.3} />
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis dataKey="time" stroke="#475569" fontSize={12} />
<YAxis stroke="#475569" fontSize={12} domain={[0, 100]} />
<Tooltip
contentStyle={{
backgroundColor: '#0f172a',
border: '1px solid #1e293b',
borderRadius: '6px',
color: '#f8fafc',
}}
/>
<Area
type="monotone"
dataKey="cpu"
stroke="#3b82f6"
fillOpacity={1}
fill="url(#colorCpu)"
name="CPU(%)"
/>
<Area
type="monotone"
dataKey="memory"
stroke="#10b981"
fillOpacity={1}
fill="url(#colorMemory)"
name="内存(%)"
/>
</AreaChart>
</ResponsiveContainer>
</div>
<div className="flex justify-center gap-6 mt-2 text-xs text-slate-400">
<span>CPU: <span className="text-blue-400 font-bold">{clusterMetrics.cpuPct.toFixed(1)}%</span></span>
<span>内存: <span className="text-emerald-400 font-bold">{clusterMetrics.memPct.toFixed(1)}%</span></span>
</div>
</CardContent>
</Card>
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader className="pb-2">
<CardTitle className="text-sm text-slate-400">资源分布</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div className="text-center p-4 rounded-lg bg-slate-800">
<div className="text-3xl font-bold text-blue-400">{cluster.pod_count || 0}</div>
<div className="text-sm text-slate-500 mt-1">Pod</div>
</div>
<div className="text-center p-4 rounded-lg bg-slate-800">
<div className="text-3xl font-bold text-emerald-400">{cluster.service_count || 0}</div>
<div className="text-sm text-slate-500 mt-1">Service</div>
</div>
<div className="text-center p-4 rounded-lg bg-slate-800">
<div className="text-3xl font-bold text-purple-400">{cluster.deployment_count || 0}</div>
<div className="text-sm text-slate-500 mt-1">Deployment</div>
</div>
<div className="text-center p-4 rounded-lg bg-slate-800">
<div className="text-3xl font-bold text-amber-400">{cluster.node_count || 0}</div>
<div className="text-sm text-slate-500 mt-1">节点</div>
</div>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="nodes" className="mt-4">
<div className="border rounded-lg border-slate-700 overflow-hidden">
<table className="w-full">
<thead className="bg-slate-800">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">角色</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">状态</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">CPU</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">内存</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">Pod</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">操作系统</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{nodes.length === 0 ? (
<tr>
<td colSpan={7} className="px-4 py-8 text-center text-slate-500">暂无节点数据</td>
</tr>
) : (
nodes.map((node) => (
<tr key={node.id || node.name} className="hover:bg-slate-800/50">
<td className="px-4 py-3 text-sm text-slate-200">{node.name}</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
node.role === 'master'
? 'border-purple-500/30 text-purple-400 bg-purple-500/10'
: 'border-blue-500/30 text-blue-400 bg-blue-500/10'
}
>
{node.role === 'master' ? 'Master' : 'Worker'}
</Badge>
</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
(node.status || '').toLowerCase() === 'ready'
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
: 'border-red-500/30 text-red-400 bg-red-500/10'
}
>
{(node.status || '').toLowerCase() === 'ready' ? 'Ready' : 'NotReady'}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-slate-300">
{node.cpu_usage_m || '0m'} / {node.cpu}核
</td>
<td className="px-4 py-3 text-sm text-slate-300">
{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-slate-300">{node.max_pods ?? 0}</td>
<td className="px-4 py-3 text-sm text-slate-400">{node.os}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</TabsContent>
<TabsContent value="pods" className="mt-4">
<div className="border rounded-lg border-slate-700 overflow-hidden">
<table className="w-full">
<thead className="bg-slate-800">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">命名空间</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">状态</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">重启次数</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">CPU</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">内存</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">节点</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{pods.length === 0 ? (
<tr>
<td colSpan={7} className="px-4 py-8 text-center text-slate-500">暂无Pod数据</td>
</tr>
) : (
pods.map((pod) => (
<tr key={pod.id || pod.name} className="hover:bg-slate-800/50">
<td className="px-4 py-3 text-sm text-slate-200">{pod.name}</td>
<td className="px-4 py-3">
<Badge variant="outline" className="border-slate-700 text-slate-400">
{pod.namespace}
</Badge>
</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
(pod.status || '').toLowerCase() === 'running'
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
: (pod.status || '').toLowerCase() === 'pending'
? 'border-amber-500/30 text-amber-400 bg-amber-500/10'
: 'border-red-500/30 text-red-400 bg-red-500/10'
}
>
{pod.status}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-slate-300">{pod.restart_count ?? 0}</td>
<td className="px-4 py-3 text-sm text-slate-300">{pod.cpu_usage_m || '0m'}</td>
<td className="px-4 py-3 text-sm text-slate-300">{pod.mem_usage_mi || '0Mi'}</td>
<td className="px-4 py-3 text-sm text-slate-400">{pod.node_name}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</TabsContent>
<TabsContent value="deployments" className="mt-4">
<div className="border rounded-lg border-slate-700 overflow-hidden">
<table className="w-full">
<thead className="bg-slate-800">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">命名空间</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">副本数</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">可用副本</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">镜像</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">创建时间</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{deployments.length === 0 ? (
<tr>
<td colSpan={6} className="px-4 py-8 text-center text-slate-500">暂无Deployment数据</td>
</tr>
) : (
deployments.map((deployment) => (
<tr key={deployment.id || deployment.name} className="hover:bg-slate-800/50">
<td className="px-4 py-3 text-sm text-slate-200">{deployment.name}</td>
<td className="px-4 py-3">
<Badge variant="outline" className="border-slate-700 text-slate-400">
{deployment.namespace}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-slate-300">{deployment.replicas}</td>
<td className="px-4 py-3">
<span className={`text-sm ${(deployment.available_replicas ?? deployment.availableReplicas) === deployment.replicas ? 'text-emerald-400' : 'text-amber-400'}`}>
{deployment.available_replicas ?? deployment.availableReplicas} / {deployment.replicas}
</span>
</td>
<td className="px-4 py-3 text-sm text-slate-400 truncate max-w-[200px]">{deployment.image}</td>
<td className="px-4 py-3 text-sm text-slate-400">{deployment.created_at || deployment.createdAt}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</TabsContent>
<TabsContent value="services" className="mt-4">
<div className="border rounded-lg border-slate-700 overflow-hidden">
<table className="w-full">
<thead className="bg-slate-800">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">命名空间</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">类型</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">ClusterIP</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">端口</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">Selector</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{services.length === 0 ? (
<tr>
<td colSpan={6} className="px-4 py-8 text-center text-slate-500">暂无Service数据</td>
</tr>
) : (
services.map((service) => (
<tr key={service.id || service.name} className="hover:bg-slate-800/50">
<td className="px-4 py-3 text-sm text-slate-200">{service.name}</td>
<td className="px-4 py-3">
<Badge variant="outline" className="border-slate-700 text-slate-400">
{service.namespace}
</Badge>
</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
service.type === 'ClusterIP'
? 'border-blue-500/30 text-blue-400 bg-blue-500/10'
: service.type === 'NodePort'
? 'border-purple-500/30 text-purple-400 bg-purple-500/10'
: 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
}
>
{service.type}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-slate-300">{service.cluster_ip ?? service.clusterIP}</td>
<td className="px-4 py-3 text-sm text-slate-300">{service.ports}</td>
<td className="px-4 py-3 text-sm text-slate-400">{service.selector}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</TabsContent>
<TabsContent value="ingresses" className="mt-4">
<div className="border rounded-lg border-slate-700 overflow-hidden">
<table className="w-full">
<thead className="bg-slate-800">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">命名空间</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">Hosts</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">后端服务</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">TLS</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{ingresses.length === 0 ? (
<tr>
<td colSpan={5} className="px-4 py-8 text-center text-slate-500">暂无Ingress数据</td>
</tr>
) : (
ingresses.map((ingress) => (
<tr key={ingress.id || ingress.name} className="hover:bg-slate-800/50">
<td className="px-4 py-3 text-sm text-slate-200">{ingress.name}</td>
<td className="px-4 py-3">
<Badge variant="outline" className="border-slate-700 text-slate-400">
{ingress.namespace}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-slate-300">{ingress.hosts}</td>
<td className="px-4 py-3 text-sm text-slate-300">{ingress.backend}</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
ingress.tls
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
: 'border-slate-700 text-slate-400'
}
>
{ingress.tls ? '已启用' : '未启用'}
</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: '2分钟前' },
{ type: 'Normal', reason: 'Created', message: 'Created pod nginx-deployment-xxx', time: '5分钟前' },
{ type: 'Warning', reason: 'FailedScheduling', message: '0/3 nodes are available', time: '10分钟前' },
{ type: 'Normal', reason: 'Pulling', message: 'Pulling image "nginx:latest"', time: '15分钟前' },
{ type: 'Normal', reason: 'Pulled', message: 'Successfully pulled image', time: '12分钟前' },
].map((event, index) => (
<div
key={index}
className="flex items-center gap-3 p-3 rounded-lg bg-slate-800/50"
>
<Badge
variant="outline"
className={
event.type === 'Normal'
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
: 'border-amber-500/30 text-amber-400 bg-amber-500/10'
}
>
{event.type}
</Badge>
<span className="text-sm font-medium text-slate-300 min-w-[120px]">{event.reason}</span>
<span className="text-sm text-slate-400 flex-1">{event.message}</span>
<span className="text-xs text-slate-500">{event.time}</span>
</div>
))}
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
);
}