feat: 修复仪表盘数据管线 — 补充应用发布种子数据 + 修复limit参数解析 + LDAP认证集成 + User类型完善

后端改进:
- 修复 dashboard/recent-alerts 的 limit 参数解析 (原代码错误地将数字解析为日期)
- 为 dashboard/recent-releases 添加 limit 参数解析支持
- 新增 generateAppReleases 种子数据生成函数 (10条应用发布记录)
- 修复 AlertEvent 的 rule_name/rule_id/severity 数据一致性
- 添加 go-ldap/ldap/v3 依赖并集成 LDAP 认证处理器
- 添加 seed.go 自动种子数据初始化 (admin用户+LDAP配置)
- User model 添加 AuthSource/LDAPConfig 字段

前端改进:
- 修复 User 类型定义以匹配后端 API (real_name/is_admin/auth_source等)
- 修复 Header.tsx 的 real_name 属性引用导致 TypeScript 编译错误
- 统一 API 服务层、暗色主题样式、组件联调优化

验证:
- go build ./... 编译通过
- npm run build 构建通过
- 仪表盘发布历史、告警消息等区域数据正常展示
- 健康检查 /health 正常
This commit is contained in:
attacker committed 2026-06-14 02:11:10 +08:00
1 parent 97c4207ff8
commit ff0b268984
39 files changed
+4283 -1878

No files matched your search

+277 -162
View File
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { cn } from '@/lib/utils';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -43,7 +43,7 @@ import {
Search,
Filter,
} from 'lucide-react';
import { k8sClusters, k8sNodes, k8sPods, k8sDeployments, k8sServices, k8sIngresses } from '@/mocks/data';
import { k8sApi } from '@/services/api';
import type { K8sCluster } from '@/types';
import {
LineChart,
@@ -61,13 +61,79 @@ import {
} from 'recharts';
export function K8sClusters() {
const [clusters, setClusters] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [selectedCluster, setSelectedCluster] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const filteredClusters = k8sClusters.filter(
// 集群详情关联资源
const [nodes, setNodes] = useState<any[]>([]);
const [pods, setPods] = useState<any[]>([]);
const [deployments, setDeployments] = useState<any[]>([]);
const [services, setServices] = useState<any[]>([]);
const [ingresses, setIngresses] = useState<any[]>([]);
const loadData = async () => {
try {
setLoading(true);
const res: any = await k8sApi.getClusters();
setClusters(res?.list || []);
} catch (e) {
console.error('加载集群列表失败:', e);
setClusters([]);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData();
}, []);
const loadClusterDetail = async (clusterId: string) => {
try {
const [nodesRes, podsRes, depRes, svcRes, ingRes] = await Promise.all([
k8sApi.getNodes(clusterId).catch(() => ({ list: [] })),
k8sApi.getPods(clusterId).catch(() => ({ list: [] })),
k8sApi.getDeployments(clusterId).catch(() => ({ list: [] })),
k8sApi.getServices(clusterId).catch(() => ({ list: [] })),
k8sApi.getIngresses(clusterId).catch(() => ({ list: [] })),
]);
setNodes((nodesRes as any)?.list || []);
setPods((podsRes as any)?.list || []);
setDeployments((depRes as any)?.list || []);
setServices((svcRes as any)?.list || []);
setIngresses((ingRes as any)?.list || []);
} catch (e) {
console.error('加载集群详情失败:', e);
}
};
useEffect(() => {
if (selectedCluster) {
loadClusterDetail(selectedCluster);
} else {
setNodes([]);
setPods([]);
setDeployments([]);
setServices([]);
setIngresses([]);
}
}, [selectedCluster]);
const handleDelete = async (id: string) => {
try {
await k8sApi.deleteCluster(id);
loadData();
} catch (e) {
console.error('删除集群失败:', e);
}
};
const filteredClusters = clusters.filter(
(cluster) =>
cluster.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
cluster.version.includes(searchQuery)
(cluster.name || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
(cluster.version || '').includes(searchQuery)
);
const getStatusBadge = (status: K8sCluster['status']) => {
@@ -98,15 +164,7 @@ export function K8sClusters() {
}
};
const clusterDetail = selectedCluster
? k8sClusters.find((c) => c.id === selectedCluster)
: null;
const clusterNodes = selectedCluster
? k8sNodes.filter((n) => n.clusterId === selectedCluster)
: [];
const clusterPods = selectedCluster
? k8sPods.filter((p) => p.clusterId === selectedCluster)
: [];
const clusterDetail = selectedCluster ? clusters.find((c) => c.id === selectedCluster) : null;
// 模拟资源使用趋势数据
const resourceData = [
@@ -128,8 +186,12 @@ export function K8sClusters() {
<p className="text-sm text-slate-500 mt-1">管理Kubernetes集群资源</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800">
<RefreshCw className="h-4 w-4 mr-2" />
<Button
variant="outline"
className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800"
onClick={loadData}
>
<RefreshCw className={cn('h-4 w-4 mr-2', loading && 'animate-spin')} />
刷新
</Button>
<Button className="bg-blue-600 hover:bg-blue-700 text-white">
@@ -146,7 +208,7 @@ export function K8sClusters() {
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">集群总数</p>
<p className="text-2xl font-bold text-slate-100">{k8sClusters.length}</p>
<p className="text-2xl font-bold text-slate-100">{clusters.length}</p>
</div>
<div className="p-3 rounded-lg bg-blue-500/10 text-blue-400">
<Cloud className="h-5 w-5" />
@@ -160,7 +222,7 @@ export function K8sClusters() {
<div>
<p className="text-sm text-slate-500">健康集群</p>
<p className="text-2xl font-bold text-emerald-400">
{k8sClusters.filter((c) => c.status === 'healthy').length}
{clusters.filter((c) => c.status === 'healthy').length}
</p>
</div>
<div className="p-3 rounded-lg bg-emerald-500/10 text-emerald-400">
@@ -175,7 +237,7 @@ export function K8sClusters() {
<div>
<p className="text-sm text-slate-500">总节点数</p>
<p className="text-2xl font-bold text-slate-100">
{k8sClusters.reduce((acc, c) => acc + c.nodes, 0)}
{clusters.reduce((acc, c) => acc + (c.node_count || 0), 0)}
</p>
</div>
<div className="p-3 rounded-lg bg-purple-500/10 text-purple-400">
@@ -190,7 +252,7 @@ export function K8sClusters() {
<div>
<p className="text-sm text-slate-500">总Pod数</p>
<p className="text-2xl font-bold text-slate-100">
{k8sClusters.reduce((acc, c) => acc + c.pods, 0)}
{clusters.reduce((acc, c) => acc + (c.pod_count || 0), 0)}
</p>
</div>
<div className="p-3 rounded-lg bg-cyan-500/10 text-cyan-400">
@@ -216,116 +278,139 @@ export function K8sClusters() {
{/* Clusters Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4">
{filteredClusters.map((cluster) => (
<Card
key={cluster.id}
className={cn(
'bg-slate-900 border-slate-800 cursor-pointer transition-all',
selectedCluster === cluster.id
? 'border-blue-500 ring-1 ring-blue-500/30'
: 'hover:border-slate-700'
)}
onClick={() => setSelectedCluster(cluster.id)}
>
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center">
<Cloud className="h-5 w-5 text-blue-400" />
</div>
<div>
<CardTitle className="text-base text-slate-100">{cluster.name}</CardTitle>
<CardDescription className="text-slate-500">{cluster.version}</CardDescription>
</div>
</div>
<div className="flex items-center gap-1">
{getStatusBadge(cluster.status)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-slate-100"
onClick={(e) => e.stopPropagation()}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48 bg-slate-800 border-slate-700">
<DropdownMenuLabel className="text-slate-300">操作</DropdownMenuLabel>
<DropdownMenuSeparator className="bg-slate-700" />
<DropdownMenuItem className="text-slate-300 focus:bg-slate-700 focus:text-slate-100">
<Eye className="h-4 w-4 mr-2" />
查看详情
</DropdownMenuItem>
<DropdownMenuItem className="text-slate-300 focus:bg-slate-700 focus:text-slate-100">
<Terminal className="h-4 w-4 mr-2" />
Kubectl
</DropdownMenuItem>
<DropdownMenuItem className="text-slate-300 focus:bg-slate-700 focus:text-slate-100">
<Settings className="h-4 w-4 mr-2" />
配置
</DropdownMenuItem>
<DropdownMenuSeparator className="bg-slate-700" />
<DropdownMenuItem className="text-red-400 focus:bg-slate-700 focus:text-red-400">
<Trash2 className="h-4 w-4 mr-2" />
删除
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</CardHeader>
<CardContent className="pt-0">
<div className="grid grid-cols-4 gap-2 mb-4">
<div className="text-center p-2 rounded-lg bg-slate-800/50">
<div className="text-lg font-semibold text-slate-200">{cluster.nodes}</div>
<div className="text-xs text-slate-500">节点</div>
</div>
<div className="text-center p-2 rounded-lg bg-slate-800/50">
<div className="text-lg font-semibold text-slate-200">{cluster.pods}</div>
<div className="text-xs text-slate-500">Pod</div>
</div>
<div className="text-center p-2 rounded-lg bg-slate-800/50">
<div className="text-lg font-semibold text-slate-200">{cluster.services}</div>
<div className="text-xs text-slate-500">Service</div>
</div>
<div className="text-center p-2 rounded-lg bg-slate-800/50">
<div className="text-lg font-semibold text-slate-200">{cluster.deployments}</div>
<div className="text-xs text-slate-500">Deployment</div>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500 w-10">CPU</span>
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div
className={cn(
'h-full rounded-full',
cluster.cpuUsage > 80 ? 'bg-red-400' : cluster.cpuUsage > 60 ? 'bg-amber-400' : 'bg-emerald-400'
)}
style={{ width: `${cluster.cpuUsage}%` }}
/>
</div>
<span className="text-xs text-slate-400 w-10 text-right">{cluster.cpuUsage.toFixed(1)}%</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500 w-10">内存</span>
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div
className={cn(
'h-full rounded-full',
cluster.memoryUsage > 80 ? 'bg-red-400' : cluster.memoryUsage > 60 ? 'bg-amber-400' : 'bg-emerald-400'
)}
style={{ width: `${cluster.memoryUsage}%` }}
/>
</div>
<span className="text-xs text-slate-400 w-10 text-right">{cluster.memoryUsage.toFixed(1)}%</span>
</div>
</div>
{loading ? (
<Card className="bg-slate-900 border-slate-800 col-span-full">
<CardContent className="p-12 text-center text-slate-500">
<RefreshCw className="h-6 w-6 animate-spin mx-auto mb-3" />
加载中...
</CardContent>
</Card>
))}
) : filteredClusters.length === 0 ? (
<Card className="bg-slate-900 border-slate-800 col-span-full">
<CardContent className="p-12 text-center text-slate-500">暂无集群数据</CardContent>
</Card>
) : (
filteredClusters.map((cluster) => {
const cpuUsage = Number(cluster.cpu_usage || 0);
const memoryUsage = Number(cluster.memory_usage || 0);
return (
<Card
key={cluster.id}
className={cn(
'bg-slate-900 border-slate-800 cursor-pointer transition-all',
selectedCluster === cluster.id
? 'border-blue-500 ring-1 ring-blue-500/30'
: 'hover:border-slate-700'
)}
onClick={() => setSelectedCluster(cluster.id)}
>
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center">
<Cloud className="h-5 w-5 text-blue-400" />
</div>
<div>
<CardTitle className="text-base text-slate-100">{cluster.name}</CardTitle>
<CardDescription className="text-slate-500">{cluster.version}</CardDescription>
</div>
</div>
<div className="flex items-center gap-1">
{getStatusBadge(cluster.status)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-slate-100"
onClick={(e) => e.stopPropagation()}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48 bg-slate-800 border-slate-700">
<DropdownMenuLabel className="text-slate-300">操作</DropdownMenuLabel>
<DropdownMenuSeparator className="bg-slate-700" />
<DropdownMenuItem className="text-slate-300 focus:bg-slate-700 focus:text-slate-100">
<Eye className="h-4 w-4 mr-2" />
查看详情
</DropdownMenuItem>
<DropdownMenuItem className="text-slate-300 focus:bg-slate-700 focus:text-slate-100">
<Terminal className="h-4 w-4 mr-2" />
Kubectl
</DropdownMenuItem>
<DropdownMenuItem className="text-slate-300 focus:bg-slate-700 focus:text-slate-100">
<Settings className="h-4 w-4 mr-2" />
配置
</DropdownMenuItem>
<DropdownMenuSeparator className="bg-slate-700" />
<DropdownMenuItem
className="text-red-400 focus:bg-slate-700 focus:text-red-400"
onClick={(e) => {
e.stopPropagation();
handleDelete(cluster.id);
}}
>
<Trash2 className="h-4 w-4 mr-2" />
删除
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</CardHeader>
<CardContent className="pt-0">
<div className="grid grid-cols-4 gap-2 mb-4">
<div className="text-center p-2 rounded-lg bg-slate-800/50">
<div className="text-lg font-semibold text-slate-200">{cluster.node_count || 0}</div>
<div className="text-xs text-slate-500">节点</div>
</div>
<div className="text-center p-2 rounded-lg bg-slate-800/50">
<div className="text-lg font-semibold text-slate-200">{cluster.pod_count || 0}</div>
<div className="text-xs text-slate-500">Pod</div>
</div>
<div className="text-center p-2 rounded-lg bg-slate-800/50">
<div className="text-lg font-semibold text-slate-200">{cluster.service_count || 0}</div>
<div className="text-xs text-slate-500">Service</div>
</div>
<div className="text-center p-2 rounded-lg bg-slate-800/50">
<div className="text-lg font-semibold text-slate-200">{cluster.deployment_count || 0}</div>
<div className="text-xs text-slate-500">Deployment</div>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500 w-10">CPU</span>
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div
className={cn(
'h-full rounded-full',
cpuUsage > 80 ? 'bg-red-400' : cpuUsage > 60 ? 'bg-amber-400' : 'bg-emerald-400'
)}
style={{ width: `${cpuUsage}%` }}
/>
</div>
<span className="text-xs text-slate-400 w-10 text-right">{cpuUsage.toFixed(1)}%</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500 w-10">内存</span>
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div
className={cn(
'h-full rounded-full',
memoryUsage > 80 ? 'bg-red-400' : memoryUsage > 60 ? 'bg-amber-400' : 'bg-emerald-400'
)}
style={{ width: `${memoryUsage}%` }}
/>
</div>
<span className="text-xs text-slate-400 w-10 text-right">{memoryUsage.toFixed(1)}%</span>
</div>
</div>
</CardContent>
</Card>
);
})
)}
</div>
{/* Cluster Detail */}
@@ -338,8 +423,8 @@ export function K8sClusters() {
<div>
<CardTitle className="text-slate-100">{clusterDetail.name}</CardTitle>
<CardDescription className="text-slate-500">
{clusterDetail.version} · {clusterDetail.nodes} 节点 · 创建于{' '}
{new Date(clusterDetail.createdAt).toLocaleDateString()}
{clusterDetail.version} · {clusterDetail.node_count || 0} 节点 · 创建于{' '}
{clusterDetail.created_at ? new Date(clusterDetail.created_at).toLocaleDateString() : '-'}
</CardDescription>
</div>
</div>
@@ -361,19 +446,19 @@ export function K8sClusters() {
概览
</TabsTrigger>
<TabsTrigger value="nodes" className="data-[state=active]:bg-slate-700">
节点 ({clusterNodes.length})
节点 ({nodes.length})
</TabsTrigger>
<TabsTrigger value="pods" className="data-[state=active]:bg-slate-700">
Pod ({clusterPods.length})
Pod ({pods.length})
</TabsTrigger>
<TabsTrigger value="deployments" className="data-[state=active]:bg-slate-700">
Deployment ({k8sDeployments.length})
Deployment ({deployments.length})
</TabsTrigger>
<TabsTrigger value="services" className="data-[state=active]:bg-slate-700">
Service ({k8sServices.length})
Service ({services.length})
</TabsTrigger>
<TabsTrigger value="ingresses" className="data-[state=active]:bg-slate-700">
Ingress ({k8sIngresses.length})
Ingress ({ingresses.length})
</TabsTrigger>
<TabsTrigger value="events" className="data-[state=active]:bg-slate-700">
事件
@@ -440,19 +525,19 @@ export function K8sClusters() {
<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">{clusterDetail.pods}</div>
<div className="text-3xl font-bold text-blue-400">{clusterDetail.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">{clusterDetail.services}</div>
<div className="text-3xl font-bold text-emerald-400">{clusterDetail.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">{clusterDetail.deployments}</div>
<div className="text-3xl font-bold text-purple-400">{clusterDetail.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">{clusterDetail.nodes}</div>
<div className="text-3xl font-bold text-amber-400">{clusterDetail.node_count || 0}</div>
<div className="text-sm text-slate-500 mt-1">节点</div>
</div>
</div>
@@ -476,8 +561,13 @@ export function K8sClusters() {
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{clusterNodes.map((node) => (
<tr key={node.id} className="hover:bg-slate-800/50">
{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
@@ -504,15 +594,16 @@ export function K8sClusters() {
</Badge>
</td>
<td className="px-4 py-3 text-sm text-slate-300">
{node.cpuUsage.toFixed(1)}% / {node.cpu}核
{Number(node.cpu_usage || 0).toFixed(1)}% / {node.cpu}核
</td>
<td className="px-4 py-3 text-sm text-slate-300">
{node.memoryUsage.toFixed(1)}% / {node.memory}GB
{Number(node.memory_usage || 0).toFixed(1)}% / {node.memory}GB
</td>
<td className="px-4 py-3 text-sm text-slate-300">{node.pods}</td>
<td className="px-4 py-3 text-sm text-slate-300">{node.pods ?? node.pod_count ?? 0}</td>
<td className="px-4 py-3 text-sm text-slate-400">{node.os}</td>
</tr>
))}
))
)}
</tbody>
</table>
</div>
@@ -533,8 +624,13 @@ export function K8sClusters() {
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{clusterPods.map((pod) => (
<tr key={pod.id} className="hover:bg-slate-800/50">
{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">
@@ -555,12 +651,13 @@ export function K8sClusters() {
{pod.status}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-slate-300">{pod.restarts}</td>
<td className="px-4 py-3 text-sm text-slate-300">{pod.restarts ?? 0}</td>
<td className="px-4 py-3 text-sm text-slate-300">{pod.cpu}</td>
<td className="px-4 py-3 text-sm text-slate-300">{pod.memory}</td>
<td className="px-4 py-3 text-sm text-slate-400">{pod.node}</td>
</tr>
))}
))
)}
</tbody>
</table>
</div>
@@ -580,8 +677,13 @@ export function K8sClusters() {
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{k8sDeployments.map((deployment) => (
<tr key={deployment.id} className="hover:bg-slate-800/50">
{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">
@@ -590,14 +692,15 @@ export function K8sClusters() {
</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.availableReplicas === deployment.replicas ? 'text-emerald-400' : 'text-amber-400'}`}>
{deployment.availableReplicas} / {deployment.replicas}
<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.createdAt}</td>
<td className="px-4 py-3 text-sm text-slate-400">{deployment.created_at || deployment.createdAt}</td>
</tr>
))}
))
)}
</tbody>
</table>
</div>
@@ -617,8 +720,13 @@ export function K8sClusters() {
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{k8sServices.map((service) => (
<tr key={service.id} className="hover:bg-slate-800/50">
{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">
@@ -639,11 +747,12 @@ export function K8sClusters() {
{service.type}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-slate-300">{service.clusterIP}</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>
@@ -662,8 +771,13 @@ export function K8sClusters() {
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{k8sIngresses.map((ingress) => (
<tr key={ingress.id} className="hover:bg-slate-800/50">
{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">
@@ -685,7 +799,8 @@ export function K8sClusters() {
</Badge>
</td>
</tr>
))}
))
)}
</tbody>
</table>
</div>