fix: 仪表盘死按钮修复+K8s进度条可视化+标题简化
- Dashboard: 移除无功能的"实时监控"按钮,"刷新"按钮接上数据重载(useCallback+spin动画) - DashboardRecent: 4个"查看全部"按钮接上路由导航(告警→/alerts, 发布→/releases, 服务器→/servers, 集群→/clusters) - K8sNodes: CPU/Mem使用率从纯文本改为迷你进度条(颜色阈值:绿<60%/黄60-85%/红>85%) - K8sWorkloads: Deployment副本比用进度条可视化(满=绿/未满=黄) - K8sClusters: 页面标题"K8s集群管理"→"集群列表"
This commit is contained in:
1 parent
bf3d642b59
commit
18aa445b32
5 files changed
+104
-71
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Clock, Activity, Server, Cloud, Bell, DollarSign } from 'lucide-react';
|
||||
import { Clock, Server, Cloud, Bell, DollarSign } from 'lucide-react';
|
||||
import { dashboardStatsApi, serverApi, k8sApi } from '@/services/api';
|
||||
import { StatCard } from './DashboardStats';
|
||||
import { CostOverviewCard, TopResourceUsageCard, ResourceUsageCard, computeMonthChange } from './DashboardCharts';
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
} from './DashboardRecent';
|
||||
|
||||
export function Dashboard() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [initialLoading, setInitialLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [stats, setStats] = useState<any>(null);
|
||||
const [servers, setServers] = useState<any[]>([]);
|
||||
const [clusters, setClusters] = useState<any[]>([]);
|
||||
@@ -22,55 +23,57 @@ export function Dashboard() {
|
||||
const [expCerts, setExpCerts] = useState<any[]>([]);
|
||||
const [metrics, setMetrics] = useState<any[]>([]); // 真实 Agent 指标时间序列 // Logan.Li
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [statsRes, serversRes, clustersRes, alertsRes, releasesRes, metricsRes] = 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;
|
||||
}),
|
||||
// 获取真实 Agent 指标时间序列(过去24小时按小时聚合) // Logan.Li
|
||||
dashboardStatsApi.getMetrics(24).catch((e) => {
|
||||
console.error('Failed to fetch metrics:', e);
|
||||
return [];
|
||||
}),
|
||||
]);
|
||||
const refreshData = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
try {
|
||||
const [statsRes, serversRes, clustersRes, alertsRes, releasesRes, metricsRes] = 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;
|
||||
}),
|
||||
// 获取真实 Agent 指标时间序列(过去24小时按小时聚合) // Logan.Li
|
||||
dashboardStatsApi.getMetrics(24).catch((e) => {
|
||||
console.error('Failed to fetch metrics:', e);
|
||||
return [];
|
||||
}),
|
||||
]);
|
||||
|
||||
setStats(statsRes);
|
||||
setServers(serversRes?.list ?? []);
|
||||
setClusters(clustersRes?.list ?? []);
|
||||
setAlerts(Array.isArray(alertsRes) ? alertsRes : []);
|
||||
setReleases(Array.isArray(releasesRes) ? releasesRes : []);
|
||||
setExpCerts(statsRes?.expiring_certs ?? []);
|
||||
setMetrics(Array.isArray(metricsRes) ? metricsRes : []);
|
||||
} catch (error) {
|
||||
console.error('Failed to load dashboard data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
setStats(statsRes);
|
||||
setServers(serversRes?.list ?? []);
|
||||
setClusters(clustersRes?.list ?? []);
|
||||
setAlerts(Array.isArray(alertsRes) ? alertsRes : []);
|
||||
setReleases(Array.isArray(releasesRes) ? releasesRes : []);
|
||||
setExpCerts(statsRes?.expiring_certs ?? []);
|
||||
setMetrics(Array.isArray(metricsRes) ? metricsRes : []);
|
||||
} catch (error) {
|
||||
console.error('Failed to load dashboard data:', error);
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
setInitialLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
useEffect(() => {
|
||||
refreshData();
|
||||
}, [refreshData]);
|
||||
|
||||
if (initialLoading) {
|
||||
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" />
|
||||
@@ -92,16 +95,10 @@ export function Dashboard() {
|
||||
<h1 className="text-2xl font-bold text-slate-100">仪表盘</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">实时监控和运维概览</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">
|
||||
<Clock className="h-4 w-4 mr-2" />
|
||||
刷新
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white">
|
||||
<Activity className="h-4 w-4 mr-2" />
|
||||
实时监控
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800" onClick={refreshData}>
|
||||
<Clock className={`h-4 w-4 mr-2 ${refreshing ? 'animate-spin' : ''}`} />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
|
||||
// 告警消息卡片
|
||||
export function AlertMessagesCard({ alerts }: { alerts: any[] }) {
|
||||
const navigate = useNavigate();
|
||||
const recentAlerts = alerts.filter((a) => a.status === 'firing').slice(0, 8);
|
||||
|
||||
return (
|
||||
@@ -22,7 +24,7 @@ export function AlertMessagesCard({ alerts }: { alerts: any[] }) {
|
||||
<CardTitle className="text-slate-100">告警消息</CardTitle>
|
||||
<CardDescription className="text-slate-500">最近触发的告警事件</CardDescription>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-slate-100">
|
||||
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-slate-100" onClick={() => navigate('/alerts')}>
|
||||
查看全部
|
||||
<ArrowRight className="h-4 w-4 ml-1" />
|
||||
</Button>
|
||||
@@ -150,6 +152,7 @@ export function CertificateExpiryCard({ expCerts }: { expCerts: any[] }) {
|
||||
|
||||
// 发布历史卡片
|
||||
export function ReleaseHistoryCard({ releases }: { releases: any[] }) {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardHeader className="pb-2">
|
||||
@@ -158,7 +161,7 @@ export function ReleaseHistoryCard({ releases }: { releases: any[] }) {
|
||||
<CardTitle className="text-slate-100">发布历史</CardTitle>
|
||||
<CardDescription className="text-slate-500">今日应用发布记录</CardDescription>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-slate-100">
|
||||
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-slate-100" onClick={() => navigate('/releases')}>
|
||||
查看全部
|
||||
<ArrowRight className="h-4 w-4 ml-1" />
|
||||
</Button>
|
||||
@@ -229,6 +232,7 @@ export function ReleaseHistoryCard({ releases }: { releases: any[] }) {
|
||||
|
||||
// 服务器状态卡片
|
||||
export function ServerStatusCard({ servers }: { servers: any[] }) {
|
||||
const navigate = useNavigate();
|
||||
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;
|
||||
@@ -241,7 +245,7 @@ export function ServerStatusCard({ servers }: { servers: any[] }) {
|
||||
<CardTitle className="text-slate-100">服务器状态</CardTitle>
|
||||
<CardDescription className="text-slate-500">服务器运行状态分布</CardDescription>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-slate-100">
|
||||
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-slate-100" onClick={() => navigate('/servers')}>
|
||||
查看全部
|
||||
<ArrowRight className="h-4 w-4 ml-1" />
|
||||
</Button>
|
||||
@@ -292,6 +296,7 @@ export function ServerStatusCard({ servers }: { servers: any[] }) {
|
||||
|
||||
// K8s集群状态卡片
|
||||
export function K8sClusterCard({ clusters }: { clusters: any[] }) {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardHeader className="pb-2">
|
||||
@@ -300,7 +305,7 @@ export function K8sClusterCard({ clusters }: { clusters: any[] }) {
|
||||
<CardTitle className="text-slate-100">K8s集群</CardTitle>
|
||||
<CardDescription className="text-slate-500">Kubernetes集群健康状态</CardDescription>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-slate-100">
|
||||
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-slate-100" onClick={() => navigate('/clusters')}>
|
||||
查看全部
|
||||
<ArrowRight className="h-4 w-4 ml-1" />
|
||||
</Button>
|
||||
|
||||
@@ -110,7 +110,7 @@ export function K8sClusters() {
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-100">K8s集群管理</h1>
|
||||
<h1 className="text-2xl font-bold text-slate-100">集群列表</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">管理Kubernetes集群资源</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -145,10 +145,30 @@ export function K8sNodes() {
|
||||
{n.cpu ? `${n.cpu}核` : '-'} / {n.memory ? `${Math.round(n.memory / 1024)}GB` : '-'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col gap-0.5 text-xs">
|
||||
<span className="text-blue-400">CPU {(n.cpu_usage || 0).toFixed(1)}%</span>
|
||||
<span className="text-emerald-400">Mem {(n.memory_usage || 0).toFixed(1)}%</span>
|
||||
</div>
|
||||
{(() => {
|
||||
const cpu = n.cpu_usage || 0;
|
||||
const mem = n.memory_usage || 0;
|
||||
const bar = (val: number) => val >= 85 ? 'bg-red-500' : val >= 60 ? 'bg-yellow-500' : 'bg-emerald-500';
|
||||
const txt = (val: number) => val >= 85 ? 'text-red-400' : val >= 60 ? 'text-yellow-400' : 'text-emerald-400';
|
||||
return (
|
||||
<div className="flex flex-col gap-1 min-w-[120px]">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-500 w-7">CPU</span>
|
||||
<div className="flex-1 h-1.5 rounded-full bg-slate-700/50 overflow-hidden">
|
||||
<div className={`h-full rounded-full ${bar(cpu)}`} style={{ width: `${Math.min(cpu, 100)}%` }} />
|
||||
</div>
|
||||
<span className={`text-xs w-9 text-right ${txt(cpu)}`}>{cpu.toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-500 w-7">Mem</span>
|
||||
<div className="flex-1 h-1.5 rounded-full bg-slate-700/50 overflow-hidden">
|
||||
<div className={`h-full rounded-full ${bar(mem)}`} style={{ width: `${Math.min(mem, 100)}%` }} />
|
||||
</div>
|
||||
<span className={`text-xs w-9 text-right ${txt(mem)}`}>{mem.toFixed(0)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{n.pod_count || 0} / {n.max_pods || '-'}</TableCell>
|
||||
<TableCell className="text-xs text-slate-500">{n.kubelet_version || '-'}</TableCell>
|
||||
|
||||
@@ -146,9 +146,20 @@ export function K8sWorkloads() {
|
||||
<TableCell className="font-medium text-slate-200">{d.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{d.namespace}</Badge></TableCell>
|
||||
<TableCell>
|
||||
<span className={d.ready_replicas === d.replicas ? 'text-emerald-400' : 'text-yellow-400'}>
|
||||
{d.ready_replicas || 0} / {d.replicas || 0}
|
||||
</span>
|
||||
{(() => {
|
||||
const ready = d.ready_replicas || 0;
|
||||
const total = d.replicas || 0;
|
||||
const pct = total > 0 ? (ready / total) * 100 : 0;
|
||||
const done = ready === total;
|
||||
return (
|
||||
<div className="flex items-center gap-2 min-w-[100px]">
|
||||
<div className="flex-1 h-1.5 rounded-full bg-slate-700/50 overflow-hidden">
|
||||
<div className={`h-full rounded-full ${done ? 'bg-emerald-500' : 'bg-yellow-500'}`} style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
<span className={`text-xs ${done ? 'text-emerald-400' : 'text-yellow-400'}`}>{ready}/{total}</span>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-slate-500 max-w-[200px] truncate">{d.image || '-'}</TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{d.strategy || 'RollingUpdate'}</TableCell>
|
||||
|
||||
Reference in new issue
Block a user