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

+243 -213
View File
@@ -3,7 +3,6 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Separator } from '@/components/ui/separator';
import {
Server,
Cloud,
@@ -37,8 +36,37 @@ import {
BarChart,
Bar,
} from 'recharts';
import { dashboardData, servers, k8sClusters, alertEvents, appReleases, domains, generateMetricsData } from '@/mocks/data';
import type { DashboardData } from '@/types';
import { dashboardStatsApi, serverApi, k8sApi } from '@/services/api';
// 云平台颜色映射
const CLOUD_COLORS: Record<string, string> = {
'阿里云': '#ff6a00',
aliyun: '#ff6a00',
AWS: '#ff9900',
aws: '#ff9900',
'腾讯云': '#006eff',
tencent: '#006eff',
'华为云': '#ff0000',
huawei: '#ff0000',
Azure: '#0078d4',
azure: '#0078d4',
};
const FALLBACK_CLOUD_COLORS = ['#ff6a00', '#ff9900', '#006eff', '#ff0000', '#0078d4', '#10b981', '#a855f7', '#ec4899'];
// 根据平均值生成24小时趋势数据(无实时指标时使用)
function generateMetricsData(avgCpu: number, avgMemory: number, avgDisk: number, points = 24) {
const data = [];
for (let i = points - 1; i >= 0; i--) {
const d = new Date(Date.now() - i * 60 * 60 * 1000);
data.push({
time: `${d.getHours().toString().padStart(2, '0')}:00`,
cpu: Number(Math.max(0, avgCpu + (Math.random() - 0.5) * 15).toFixed(1)),
memory: Number(Math.max(0, avgMemory + (Math.random() - 0.5) * 10).toFixed(1)),
disk: Number(Math.max(0, avgDisk + (Math.random() - 0.5) * 5).toFixed(1)),
});
}
return data;
}
// 统计卡片组件
function StatCard({
@@ -109,24 +137,18 @@ function StatCard({
}
// 成本费用卡片
function CostOverviewCard() {
const costData = [
{ name: '阿里云', value: 28500, color: '#ff6a00' },
{ name: 'AWS', value: 42000, color: '#ff9900' },
{ name: '腾讯云', value: 15800, color: '#006eff' },
{ name: '华为云', value: 8900, color: '#ff0000' },
{ name: 'Azure', value: 12500, color: '#0078d4' },
];
function CostOverviewCard({ costStats }: { costStats: any }) {
const cloudCosts: any[] = costStats?.cloud_costs ?? [];
const monthTrend: any[] = costStats?.month_trend ?? [];
const monthlyData = [
{ month: '1月', cost: 85000 },
{ month: '2月', cost: 92000 },
{ month: '3月', cost: 88000 },
{ month: '4月', cost: 95000 },
{ month: '5月', cost: 107700 },
];
const totalCost = costData.reduce((sum, item) => sum + item.value, 0);
const costData = cloudCosts.map((c, i) => ({
name: c.cloud,
value: c.cost,
color: CLOUD_COLORS[c.cloud] || FALLBACK_CLOUD_COLORS[i % FALLBACK_CLOUD_COLORS.length],
}));
const monthlyData = monthTrend.map((m) => ({ month: m.month, cost: m.cost }));
const totalCost =
costStats?.total_cost ?? costData.reduce((sum, item) => sum + item.value, 0);
return (
<Card className="bg-slate-900 border-slate-800">
@@ -137,7 +159,7 @@ function CostOverviewCard() {
<CardDescription className="text-slate-500">本月各云平台费用支出</CardDescription>
</div>
<div className="text-right">
<p className="text-2xl font-bold text-slate-100">¥{totalCost.toLocaleString()}</p>
<p className="text-2xl font-bold text-slate-100">¥{Number(totalCost).toLocaleString()}</p>
<p className="text-xs text-emerald-400">+12.5% 较上月</p>
</div>
</div>
@@ -205,7 +227,7 @@ function CostOverviewCard() {
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: item.color }} />
<span className="text-sm text-slate-300">{item.name}</span>
</div>
<span className="text-sm font-medium text-slate-100">¥{item.value.toLocaleString()}</span>
<span className="text-sm font-medium text-slate-100">¥{Number(item.value).toLocaleString()}</span>
</div>
))}
</div>
@@ -215,8 +237,8 @@ function CostOverviewCard() {
}
// 告警消息卡片
function AlertMessagesCard() {
const recentAlerts = alertEvents.filter(a => a.status === 'firing').slice(0, 8);
function AlertMessagesCard({ alerts }: { alerts: any[] }) {
const recentAlerts = alerts.filter((a) => a.status === 'firing').slice(0, 8);
return (
<Card className="bg-slate-900 border-slate-800">
@@ -246,7 +268,7 @@ function AlertMessagesCard() {
}`} />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-slate-200 truncate">{alert.ruleName}</span>
<span className="text-sm font-medium text-slate-200 truncate">{alert.rule_name}</span>
<Badge
variant="outline"
className={`text-xs ${
@@ -263,7 +285,9 @@ function AlertMessagesCard() {
<p className="text-xs text-slate-500 mt-1">{alert.target}</p>
<p className="text-xs text-slate-500">{alert.message}</p>
</div>
<span className="text-xs text-slate-500 whitespace-nowrap">{alert.startedAt}</span>
<span className="text-xs text-slate-500 whitespace-nowrap">
{alert.created_at ? new Date(alert.created_at).toLocaleString() : ''}
</span>
</div>
))}
</div>
@@ -274,18 +298,53 @@ function AlertMessagesCard() {
}
// Top5 资源使用卡片
function TopResourceUsageCard() {
const topCpuServers = [...servers]
.sort((a, b) => b.cpuUsage - a.cpuUsage)
.slice(0, 5);
function TopResourceUsageCard({ topUsage }: { topUsage: any }) {
const topCpuServers = (topUsage?.cpu ?? []).slice(0, 5);
const topMemoryServers = (topUsage?.memory ?? []).slice(0, 5);
const topDiskServers = (topUsage?.disk ?? []).slice(0, 5);
const topMemoryServers = [...servers]
.sort((a, b) => b.memoryUsage - a.memoryUsage)
.slice(0, 5);
const topDiskServers = [...servers]
.sort((a, b) => b.diskUsage - a.diskUsage)
.slice(0, 5);
const renderList = (
items: any[],
color: string,
BarColor: string,
icon: React.ReactNode,
label: string
) => (
<div>
<div className="flex items-center gap-2 mb-3">
{icon}
<span className="text-sm font-medium text-slate-300">{label}</span>
</div>
<div className="space-y-2">
{items.map((item, index) => {
const usage = Number(item.usage ?? 0);
return (
<div key={item.id ?? index} className="flex items-center gap-2">
<span className="text-xs text-slate-500 w-4">{index + 1}</span>
<div className="flex-1 min-w-0">
<p className="text-xs text-slate-300 truncate">{item.hostname}</p>
<div className="flex items-center gap-2">
<div className="flex-1 h-1.5 bg-slate-700 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${
usage > 80 ? 'bg-red-400' : usage > 60 ? 'bg-amber-400' : BarColor
}`}
style={{ width: `${usage}%` }}
/>
</div>
<span className={`text-xs ${
usage > 80 ? 'text-red-400' : usage > 60 ? 'text-amber-400' : 'text-slate-400'
}`}>
{usage.toFixed(1)}%
</span>
</div>
</div>
</div>
);
})}
</div>
</div>
);
return (
<Card className="bg-slate-900 border-slate-800">
@@ -300,103 +359,11 @@ function TopResourceUsageCard() {
<CardContent>
<div className="grid grid-cols-3 gap-4">
{/* CPU Top5 */}
<div>
<div className="flex items-center gap-2 mb-3">
<Cpu className="h-4 w-4 text-blue-400" />
<span className="text-sm font-medium text-slate-300">CPU</span>
</div>
<div className="space-y-2">
{topCpuServers.map((server, index) => (
<div key={server.id} className="flex items-center gap-2">
<span className="text-xs text-slate-500 w-4">{index + 1}</span>
<div className="flex-1 min-w-0">
<p className="text-xs text-slate-300 truncate">{server.hostname}</p>
<div className="flex items-center gap-2">
<div className="flex-1 h-1.5 bg-slate-700 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${
server.cpuUsage > 80 ? 'bg-red-400' : server.cpuUsage > 60 ? 'bg-amber-400' : 'bg-blue-400'
}`}
style={{ width: `${server.cpuUsage}%` }}
/>
</div>
<span className={`text-xs ${
server.cpuUsage > 80 ? 'text-red-400' : server.cpuUsage > 60 ? 'text-amber-400' : 'text-slate-400'
}`}>
{server.cpuUsage.toFixed(1)}%
</span>
</div>
</div>
</div>
))}
</div>
</div>
{renderList(topCpuServers, 'blue', 'bg-blue-400', <Cpu className="h-4 w-4 text-blue-400" />, 'CPU')}
{/* Memory Top5 */}
<div>
<div className="flex items-center gap-2 mb-3">
<MemoryStick className="h-4 w-4 text-emerald-400" />
<span className="text-sm font-medium text-slate-300">内存</span>
</div>
<div className="space-y-2">
{topMemoryServers.map((server, index) => (
<div key={server.id} className="flex items-center gap-2">
<span className="text-xs text-slate-500 w-4">{index + 1}</span>
<div className="flex-1 min-w-0">
<p className="text-xs text-slate-300 truncate">{server.hostname}</p>
<div className="flex items-center gap-2">
<div className="flex-1 h-1.5 bg-slate-700 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${
server.memoryUsage > 80 ? 'bg-red-400' : server.memoryUsage > 60 ? 'bg-amber-400' : 'bg-emerald-400'
}`}
style={{ width: `${server.memoryUsage}%` }}
/>
</div>
<span className={`text-xs ${
server.memoryUsage > 80 ? 'text-red-400' : server.memoryUsage > 60 ? 'text-amber-400' : 'text-slate-400'
}`}>
{server.memoryUsage.toFixed(1)}%
</span>
</div>
</div>
</div>
))}
</div>
</div>
{renderList(topMemoryServers, 'emerald', 'bg-emerald-400', <MemoryStick className="h-4 w-4 text-emerald-400" />, '内存')}
{/* Disk Top5 */}
<div>
<div className="flex items-center gap-2 mb-3">
<Database className="h-4 w-4 text-purple-400" />
<span className="text-sm font-medium text-slate-300">磁盘</span>
</div>
<div className="space-y-2">
{topDiskServers.map((server, index) => (
<div key={server.id} className="flex items-center gap-2">
<span className="text-xs text-slate-500 w-4">{index + 1}</span>
<div className="flex-1 min-w-0">
<p className="text-xs text-slate-300 truncate">{server.hostname}</p>
<div className="flex items-center gap-2">
<div className="flex-1 h-1.5 bg-slate-700 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${
server.diskUsage > 80 ? 'bg-red-400' : server.diskUsage > 60 ? 'bg-amber-400' : 'bg-purple-400'
}`}
style={{ width: `${server.diskUsage}%` }}
/>
</div>
<span className={`text-xs ${
server.diskUsage > 80 ? 'text-red-400' : server.diskUsage > 60 ? 'text-amber-400' : 'text-slate-400'
}`}>
{server.diskUsage.toFixed(1)}%
</span>
</div>
</div>
</div>
))}
</div>
</div>
{renderList(topDiskServers, 'purple', 'bg-purple-400', <Database className="h-4 w-4 text-purple-400" />, '磁盘')}
</div>
</CardContent>
</Card>
@@ -404,17 +371,16 @@ function TopResourceUsageCard() {
}
// 证书到期卡片
function CertificateExpiryCard() {
const expiringDomains = domains
.filter(d => d.sslEnabled)
.map(d => ({
function CertificateExpiryCard({ expCerts }: { expCerts: any[] }) {
const expiringDomains = (expCerts ?? [])
.map((d) => ({
...d,
daysUntilExpiry: Math.ceil((new Date(d.sslExpireDate!).getTime() - Date.now()) / (1000 * 60 * 60 * 24))
daysUntilExpiry: d.days_until_expiry ?? Math.ceil((new Date(d.expire_date).getTime() - Date.now()) / (1000 * 60 * 60 * 24)),
}))
.sort((a, b) => a.daysUntilExpiry - b.daysUntilExpiry);
const criticalCount = expiringDomains.filter(d => d.daysUntilExpiry < 30).length;
const warningCount = expiringDomains.filter(d => d.daysUntilExpiry >= 30 && d.daysUntilExpiry < 60).length;
const criticalCount = expiringDomains.filter((d) => d.daysUntilExpiry < 30).length;
const warningCount = expiringDomains.filter((d) => d.daysUntilExpiry >= 30 && d.daysUntilExpiry < 60).length;
return (
<Card className="bg-slate-900 border-slate-800">
@@ -457,7 +423,7 @@ function CertificateExpiryCard() {
</div>
<div>
<p className="text-sm font-medium text-slate-200">{domain.name}</p>
<p className="text-xs text-slate-500">到期: {domain.sslExpireDate}</p>
<p className="text-xs text-slate-500">到期: {domain.expire_date}</p>
</div>
</div>
<Badge
@@ -482,8 +448,16 @@ function CertificateExpiryCard() {
}
// 资源使用率趋势卡片
function ResourceUsageCard() {
const data = generateMetricsData(24);
function ResourceUsageCard({
avgCpu,
avgMemory,
avgDisk,
}: {
avgCpu: number;
avgMemory: number;
avgDisk: number;
}) {
const data = generateMetricsData(avgCpu, avgMemory, avgDisk, 24);
return (
<Card className="bg-slate-900 border-slate-800">
@@ -574,7 +548,7 @@ function ResourceUsageCard() {
}
// 发布历史卡片
function ReleaseHistoryCard() {
function ReleaseHistoryCard({ releases }: { releases: any[] }) {
return (
<Card className="bg-slate-900 border-slate-800">
<CardHeader className="pb-2">
@@ -591,7 +565,7 @@ function ReleaseHistoryCard() {
</CardHeader>
<CardContent>
<div className="space-y-2">
{appReleases.slice(0, 5).map((release) => (
{releases.slice(0, 5).map((release) => (
<div
key={release.id}
className="flex items-center gap-3 p-3 rounded-lg bg-slate-800/50 hover:bg-slate-800 transition-colors"
@@ -611,7 +585,7 @@ function ReleaseHistoryCard() {
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-slate-200">{release.appName}</span>
<span className="text-sm font-medium text-slate-200">{release.app_name}</span>
<Badge variant="outline" className="text-xs border-slate-700 text-slate-400">
{release.version}
</Badge>
@@ -619,7 +593,7 @@ function ReleaseHistoryCard() {
<div className="flex items-center gap-2 mt-0.5">
<span className="text-xs text-slate-500">{release.environment}</span>
<span className="text-xs text-slate-500">·</span>
<span className="text-xs text-slate-500">{release.commitId}</span>
<span className="text-xs text-slate-500">{release.commit_id}</span>
</div>
</div>
<Badge
@@ -653,7 +627,7 @@ function ReleaseHistoryCard() {
}
// 服务器状态卡片
function ServerStatusCard() {
function ServerStatusCard({ servers }: { servers: any[] }) {
const runningServers = servers.filter((s) => s.status === 'running').length;
const stoppedServers = servers.filter((s) => s.status === 'stopped').length;
const maintenanceServers = servers.filter((s) => s.status === 'maintenance').length;
@@ -704,8 +678,8 @@ function ServerStatusCard() {
<p className="text-xs text-slate-500">{server.ip}</p>
</div>
<div className="text-right">
<p className="text-xs text-slate-400">CPU {server.cpuUsage.toFixed(1)}%</p>
<p className="text-xs text-slate-400">内存 {server.memoryUsage.toFixed(1)}%</p>
<p className="text-xs text-slate-400">CPU {Number(server.cpu_usage ?? 0).toFixed(1)}%</p>
<p className="text-xs text-slate-400">内存 {Number(server.memory_usage ?? 0).toFixed(1)}%</p>
</div>
</div>
))}
@@ -716,7 +690,7 @@ function ServerStatusCard() {
}
// K8s集群状态卡片
function K8sClusterCard() {
function K8sClusterCard({ clusters }: { clusters: any[] }) {
return (
<Card className="bg-slate-900 border-slate-800">
<CardHeader className="pb-2">
@@ -733,46 +707,50 @@ function K8sClusterCard() {
</CardHeader>
<CardContent>
<div className="space-y-3">
{k8sClusters.map((cluster) => (
<div key={cluster.id} className="p-3 rounded-lg bg-slate-800/50">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Cloud className="h-4 w-4 text-blue-400" />
<span className="text-sm font-medium text-slate-200">{cluster.name}</span>
{clusters.map((cluster) => {
const cpuUsage = Number(cluster.cpu_usage ?? 0);
const memoryUsage = Number(cluster.memory_usage ?? 0);
return (
<div key={cluster.id} className="p-3 rounded-lg bg-slate-800/50">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Cloud className="h-4 w-4 text-blue-400" />
<span className="text-sm font-medium text-slate-200">{cluster.name}</span>
</div>
<Badge
variant="outline"
className={`text-xs ${
cluster.status === 'healthy'
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
: cluster.status === 'warning'
? 'border-amber-500/30 text-amber-400 bg-amber-500/10'
: 'border-red-500/30 text-red-400 bg-red-500/10'
}`}
>
{cluster.status === 'healthy' ? '健康' : cluster.status === 'warning' ? '警告' : '异常'}
</Badge>
</div>
<Badge
variant="outline"
className={`text-xs ${
cluster.status === 'healthy'
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
: cluster.status === 'warning'
? 'border-amber-500/30 text-amber-400 bg-amber-500/10'
: 'border-red-500/30 text-red-400 bg-red-500/10'
}`}
>
{cluster.status === 'healthy' ? '健康' : cluster.status === 'warning' ? '警告' : '异常'}
</Badge>
</div>
<div className="grid grid-cols-4 gap-2 text-center">
<div>
<div className="text-sm font-medium text-slate-200">{cluster.nodes}</div>
<div className="text-xs text-slate-500">节点</div>
</div>
<div>
<div className="text-sm font-medium text-slate-200">{cluster.pods}</div>
<div className="text-xs text-slate-500">Pod</div>
</div>
<div>
<div className="text-sm font-medium text-slate-200">{cluster.cpuUsage.toFixed(1)}%</div>
<div className="text-xs text-slate-500">CPU</div>
</div>
<div>
<div className="text-sm font-medium text-slate-200">{cluster.memoryUsage.toFixed(1)}%</div>
<div className="text-xs text-slate-500">内存</div>
<div className="grid grid-cols-4 gap-2 text-center">
<div>
<div className="text-sm font-medium text-slate-200">{cluster.node_count ?? cluster.nodes ?? 0}</div>
<div className="text-xs text-slate-500">节点</div>
</div>
<div>
<div className="text-sm font-medium text-slate-200">{cluster.pod_count ?? cluster.pods ?? 0}</div>
<div className="text-xs text-slate-500">Pod</div>
</div>
<div>
<div className="text-sm font-medium text-slate-200">{cpuUsage.toFixed(1)}%</div>
<div className="text-xs text-slate-500">CPU</div>
</div>
<div>
<div className="text-sm font-medium text-slate-200">{memoryUsage.toFixed(1)}%</div>
<div className="text-xs text-slate-500">内存</div>
</div>
</div>
</div>
</div>
))}
);
})}
</div>
</CardContent>
</Card>
@@ -780,21 +758,69 @@ function K8sClusterCard() {
}
export function Dashboard() {
const [data, setData] = useState<DashboardData>(dashboardData);
const [loading, setLoading] = useState(true);
const [stats, setStats] = useState<any>(null);
const [servers, setServers] = useState<any[]>([]);
const [clusters, setClusters] = useState<any[]>([]);
const [alerts, setAlerts] = useState<any[]>([]);
const [releases, setReleases] = useState<any[]>([]);
const [expCerts, setExpCerts] = useState<any[]>([]);
// 模拟实时数据更新
useEffect(() => {
const interval = setInterval(() => {
setData((prev) => ({
...prev,
cpuUsage: 60 + Math.random() * 20,
memoryUsage: 70 + Math.random() * 15,
networkUsage: 30 + Math.random() * 30,
}));
}, 5000);
return () => clearInterval(interval);
const fetchData = async () => {
setLoading(true);
try {
const [statsRes, serversRes, clustersRes, alertsRes, releasesRes] = await Promise.all([
dashboardStatsApi.getStats().catch((e) => {
console.error('Failed to fetch dashboard stats:', e);
return null;
}),
serverApi.getServers({ page: 1, page_size: 100 }).catch((e) => {
console.error('Failed to fetch servers:', e);
return null;
}),
k8sApi.getClusters().catch((e) => {
console.error('Failed to fetch k8s clusters:', e);
return null;
}),
dashboardStatsApi.getAlerts(10).catch((e) => {
console.error('Failed to fetch dashboard alerts:', e);
return null;
}),
dashboardStatsApi.getReleases(5).catch((e) => {
console.error('Failed to fetch dashboard releases:', e);
return null;
}),
]);
setStats(statsRes);
setServers(serversRes?.list ?? []);
setClusters(clustersRes?.list ?? []);
setAlerts(Array.isArray(alertsRes) ? alertsRes : []);
setReleases(Array.isArray(releasesRes) ? releasesRes : []);
setExpCerts(statsRes?.expiring_certs ?? []);
} catch (error) {
console.error('Failed to load dashboard data:', error);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (loading) {
return (
<div className="flex items-center justify-center h-96">
<div className="h-8 w-8 border-2 border-slate-700 border-t-blue-500 rounded-full animate-spin" />
</div>
);
}
const serverStats = stats?.server_stats;
const k8sStats = stats?.k8s_stats;
const alertStats = stats?.alert_stats;
const costStats = stats?.cost_stats;
return (
<div className="space-y-6">
{/* Page Header */}
@@ -819,8 +845,8 @@ export function Dashboard() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
title="服务器总数"
value={data.serverCount}
description={`${data.runningServers} 台运行中`}
value={serverStats?.total ?? 0}
description={`${serverStats?.running ?? 0} 台运行中`}
icon={Server}
trend="up"
trendValue="+3 本月"
@@ -828,8 +854,8 @@ export function Dashboard() {
/>
<StatCard
title="K8s集群"
value={data.k8sClusters}
description={`${data.healthyClusters} 个健康`}
value={k8sStats?.total ?? 0}
description={`${k8sStats?.healthy ?? 0} 个健康`}
icon={Cloud}
trend="neutral"
trendValue="稳定"
@@ -837,8 +863,8 @@ export function Dashboard() {
/>
<StatCard
title="待处理告警"
value={data.alertCount}
description={`${data.criticalAlerts} 个严重`}
value={alertStats?.firing ?? 0}
description={`${alertStats?.critical ?? 0} 个严重`}
icon={Bell}
trend="down"
trendValue="-5 本周"
@@ -846,7 +872,7 @@ export function Dashboard() {
/>
<StatCard
title="本月云成本"
value="¥107,700"
value={`¥${Number(costStats?.month_cost ?? 0).toLocaleString()}`}
description="较上月 +12.5%"
icon={DollarSign}
trend="up"
@@ -856,25 +882,29 @@ export function Dashboard() {
</div>
{/* Resource Usage Chart */}
<ResourceUsageCard />
<ResourceUsageCard
avgCpu={Number(serverStats?.avg_cpu ?? 0)}
avgMemory={Number(serverStats?.avg_memory ?? 0)}
avgDisk={Number(serverStats?.avg_disk ?? 0)}
/>
{/* Cost & Top Resource Usage */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<CostOverviewCard />
<TopResourceUsageCard />
<CostOverviewCard costStats={costStats} />
<TopResourceUsageCard topUsage={stats?.top_resource_usage} />
</div>
{/* Alert Messages & Certificate Expiry */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<AlertMessagesCard />
<CertificateExpiryCard />
<AlertMessagesCard alerts={alerts} />
<CertificateExpiryCard expCerts={expCerts} />
</div>
{/* Bottom Row */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<ReleaseHistoryCard />
<ServerStatusCard />
<K8sClusterCard />
<ReleaseHistoryCard releases={releases} />
<ServerStatusCard servers={servers} />
<K8sClusterCard clusters={clusters} />
</div>
</div>
);