feat: 监控大屏 Grafana 风格增强 — 6卡统计 + bargauge 资源条状图 (参照 k8s/node/jvm dashboard)

- 统计卡片: 3→6 (主机总数/在线/离线/平均CPU/平均内存) 带阈值颜色
- 资源条状图: Top 5 CPU + 内存排行, 渐变色进度条 (绿<60%<黄<85%<红)
- 参照: Grafana k8s-dashboard bargauge / node-dashboard 表格 / jvm Quick Facts
This commit is contained in:
attacker committed 2026-06-18 14:04:26 +00:00
1 parent 2596bbc789
commit d2201fc8c0
1 file changed
+131 -16
@@ -1,10 +1,11 @@
// 主机监控大屏 — 列表视图 + 可排序 + 操作栏 // Logan.Li
// 主机监控大屏 — Grafana 风格:统计卡片 + 资源条状图 + 可排序列表 // Logan.Li
// 参照: Grafana node-dashboard.json (表格+时序) / k8s-dashboard.json (bargauge+阈值)
import { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Server, Cpu, HardDrive, Activity, Monitor, Bell,
ArrowUpDown, ArrowUp, ArrowDown, RefreshCw, Search,
ChevronLeft, ChevronRight
ChevronLeft, ChevronRight, Zap, AlertTriangle, Wifi, WifiOff
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -133,36 +134,150 @@ export default function HostMonitorDashboard() {
return (
<div className="space-y-4">
{/* 统计卡片 */}
<div className="grid grid-cols-3 gap-3">
{/* 统计卡片 — Grafana Quick Facts 风格 (参照 JVM Dashboard) */}
<div className="grid grid-cols-6 gap-3">
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4 flex items-center gap-3">
<div className="p-2 rounded-lg bg-emerald-500/10"><Server className="h-5 w-5 text-emerald-400" /></div>
<CardContent className="p-3 flex items-center gap-2.5">
<div className="p-1.5 rounded-lg bg-blue-500/10"><Server className="h-4 w-4 text-blue-400" /></div>
<div>
<div className="text-2xl font-bold text-[var(--dg-text-primary)]">{stats.online}</div>
<div className="text-xs text-[var(--dg-text-muted)]">Agent 在线</div>
<div className="text-xl font-bold text-[var(--dg-text-primary)]">{stats.total}</div>
<div className="text-[10px] text-[var(--dg-text-muted)]">主机总数</div>
</div>
</CardContent>
</Card>
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4 flex items-center gap-3">
<div className="p-2 rounded-lg bg-slate-500/10"><Server className="h-5 w-5 text-slate-400" /></div>
<CardContent className="p-3 flex items-center gap-2.5">
<div className="p-1.5 rounded-lg bg-emerald-500/10"><Wifi className="h-4 w-4 text-emerald-400" /></div>
<div>
<div className="text-2xl font-bold text-[var(--dg-text-primary)]">{stats.offline}</div>
<div className="text-xs text-[var(--dg-text-muted)]">Agent 离线</div>
<div className="text-xl font-bold text-emerald-400">{stats.online}</div>
<div className="text-[10px] text-[var(--dg-text-muted)]">Agent 在线</div>
</div>
</CardContent>
</Card>
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4 flex items-center gap-3">
<div className="p-2 rounded-lg bg-blue-500/10"><Monitor className="h-5 w-5 text-blue-400" /></div>
<CardContent className="p-3 flex items-center gap-2.5">
<div className="p-1.5 rounded-lg bg-slate-500/10"><WifiOff className="h-4 w-4 text-slate-400" /></div>
<div>
<div className="text-2xl font-bold text-[var(--dg-text-primary)]">{stats.total}</div>
<div className="text-xs text-[var(--dg-text-muted)]">主机总数</div>
<div className="text-xl font-bold text-slate-400">{stats.offline}</div>
<div className="text-[10px] text-[var(--dg-text-muted)]">离线</div>
</div>
</CardContent>
</Card>
{(() => {
const onlines = hosts.filter((h: any) => h.agent_status === 'online');
const avgCpu = onlines.length > 0 ? onlines.reduce((s: number, h: any) => s + (h.cpu_usage || 0), 0) / onlines.length : 0;
const avgMem = onlines.length > 0 ? onlines.reduce((s: number, h: any) => s + (h.memory_usage || 0), 0) / onlines.length : 0;
const cpuColor = avgCpu > 85 ? 'text-red-400' : avgCpu > 60 ? 'text-amber-400' : 'text-emerald-400';
const memColor = avgMem > 85 ? 'text-red-400' : avgMem > 60 ? 'text-amber-400' : 'text-emerald-400';
return (<>
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-3 flex items-center gap-2.5">
<div className="p-1.5 rounded-lg bg-red-500/10"><Cpu className="h-4 w-4 text-red-400" /></div>
<div>
<div className={`text-xl font-bold ${cpuColor}`}>{avgCpu.toFixed(1)}<span className="text-sm">%</span></div>
<div className="text-[10px] text-[var(--dg-text-muted)]">平均 CPU</div>
</div>
</CardContent>
</Card>
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-3 flex items-center gap-2.5">
<div className="p-1.5 rounded-lg bg-amber-500/10"><Server className="h-4 w-4 text-amber-400" /></div>
<div>
<div className={`text-xl font-bold ${memColor}`}>{avgMem.toFixed(1)}<span className="text-sm">%</span></div>
<div className="text-[10px] text-[var(--dg-text-muted)]">平均内存</div>
</div>
</CardContent>
</Card>
</>);
})()}
</div>
{/* 资源条状图 — Grafana Bargauge 风格 (参照 K8s Node CPU/Memory Ratio) */}
{hosts.filter((h: any) => h.agent_status === 'online').length > 0 && (
<div className="grid grid-cols-2 gap-3">
{/* CPU 排行 */}
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardHeader className="pb-2">
<CardTitle className="text-sm flex items-center gap-2 text-[var(--dg-text-primary)]">
<Cpu className="h-4 w-4 text-red-400" />
CPU 使用率 Top 5
<Badge variant="outline" className="text-[10px] bg-red-500/10 text-red-400 border-red-500/30">阈值: 60% / 85%</Badge>
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{[...hosts]
.filter((h: any) => h.agent_status === 'online')
.sort((a: any, b: any) => (b.cpu_usage || 0) - (a.cpu_usage || 0))
.slice(0, 5)
.map((h: any) => {
const val = h.cpu_usage || 0;
return (
<div key={h.id} className="flex items-center gap-2">
<span className="text-xs text-[var(--dg-text-secondary)] w-20 truncate" title={h.hostname}>{h.hostname}</span>
<div className="flex-1 h-4 rounded bg-slate-700/50 overflow-hidden">
<div
className="h-full rounded transition-all"
style={{
width: `${Math.min(val, 100)}%`,
background: val >= 85
? 'linear-gradient(90deg, #ef4444, #f87171)'
: val >= 60
? 'linear-gradient(90deg, #f59e0b, #fbbf24)'
: 'linear-gradient(90deg, #10b981, #34d399)'
}}
/>
</div>
<span className={`text-xs font-mono w-10 text-right ${
val >= 85 ? 'text-red-400' : val >= 60 ? 'text-amber-400' : 'text-emerald-400'
}`}>{val.toFixed(1)}%</span>
</div>
);
})}
</CardContent>
</Card>
{/* 内存排行 */}
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardHeader className="pb-2">
<CardTitle className="text-sm flex items-center gap-2 text-[var(--dg-text-primary)]">
<Server className="h-4 w-4 text-amber-400" />
内存使用率 Top 5
<Badge variant="outline" className="text-[10px] bg-amber-500/10 text-amber-400 border-amber-500/30">阈值: 60% / 85%</Badge>
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{[...hosts]
.filter((h: any) => h.agent_status === 'online')
.sort((a: any, b: any) => (b.memory_usage || 0) - (a.memory_usage || 0))
.slice(0, 5)
.map((h: any) => {
const val = h.memory_usage || 0;
return (
<div key={h.id} className="flex items-center gap-2">
<span className="text-xs text-[var(--dg-text-secondary)] w-20 truncate" title={h.hostname}>{h.hostname}</span>
<div className="flex-1 h-4 rounded bg-slate-700/50 overflow-hidden">
<div
className="h-full rounded transition-all"
style={{
width: `${Math.min(val, 100)}%`,
background: val >= 85
? 'linear-gradient(90deg, #ef4444, #f87171)'
: val >= 60
? 'linear-gradient(90deg, #f59e0b, #fbbf24)'
: 'linear-gradient(90deg, #10b981, #34d399)'
}}
/>
</div>
<span className={`text-xs font-mono w-10 text-right ${
val >= 85 ? 'text-red-400' : val >= 60 ? 'text-amber-400' : 'text-emerald-400'
}`}>{val.toFixed(1)}%</span>
</div>
);
})}
</CardContent>
</Card>
</div>
)}
{/* 表格卡片 */}
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">