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:
1 parent
97c4207ff8
commit
ff0b268984
39 files changed
+4283
-1878
No files matched your search
@@ -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>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user