- 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 回归通过
514 lines
26 KiB
TypeScript
514 lines
26 KiB
TypeScript
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>
|
||
);
|
||
}
|