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:
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)]">
|
||||
|
||||
Reference in new issue
Block a user