feat: 监控大屏 /grafana→/dashboards + Agent主机Top10默认面板

- 路由: /grafana → /dashboards (Sidebar + App.tsx)
- GrafanaDashboard 新增默认面板: Agent 主机监控 Top 10
  - CPU/Memory/Disk 使用率三列排行
  - 进度条颜色: >85%红 >60%黄 <60%绿
  - 🥇🥈🥉 emoji 前三名
  - 30s 自动刷新
- Prometheus/Zabbix/SkyWalking 需配置数据源后才可用
This commit is contained in:
attacker committed 2026-06-18 13:26:38 +00:00
1 parent 737f00169f
commit 2bf3e7c308
3 files changed
+92 -3

No files matched your search

+1 -1
View File
@@ -73,7 +73,7 @@ function App() {
<Route path="metrics" element={<MetricsExplorer />} />
<Route path="alert-center" element={<AlertCenter />} />
<Route path="trace" element={<TraceManager />} />
<Route path="grafana" element={<GrafanaDashboard />} />
<Route path="dashboards" element={<GrafanaDashboard />} />
<Route path="alerts" element={<AlertManager />} />
<Route path="logs" element={<AlertManager />} />
<Route path="approval" element={<ApprovalFlowManager />} />
+1 -1
View File
@@ -88,7 +88,7 @@ const menuItems: MenuItem[] = [
children: [
{ id: 'monitor-center', name: '资源监控', icon: Activity, path: '/monitor-center' },
{ id: 'metrics', name: '指标监控', icon: Cpu, path: '/metrics' },
{ id: 'grafana', name: '监控大屏', icon: Monitor, path: '/grafana' },
{ id: 'grafana', name: '监控大屏', icon: Monitor, path: '/dashboards' },
{ id: 'trace', name: '链路追踪', icon: GitBranch, path: '/trace' },
{ id: 'alert-center', name: '告警服务', icon: Bell, path: '/alert-center' },
],
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import {
Monitor, Settings, Plus, Edit, Trash2, Copy, Eye, Download, RefreshCw,
Maximize2, Minimize2, Save, X, ChevronDown, BarChart3, LineChart, PieChart,
Activity, Cpu, Database, Server, Globe, Zap, AlertTriangle, TrendingUp,
Activity, Cpu, Database, Server, Globe, Zap, AlertTriangle, TrendingUp, HardDrive,
TrendingDown, Clock, Calendar, Filter, Search
} from 'lucide-react';
import { Button } from '@/components/ui/button';
@@ -23,6 +23,7 @@ import { toast } from 'sonner';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { dashboardApi } from '@/services/api';
import { GrafanaPanelContent } from './GrafanaPanelContent';
import { http } from '@/services/http';
interface Dashboard {
id: string;
@@ -107,6 +108,38 @@ const GrafanaDashboard: React.FC = () => {
url: ''
});
// Agent 主机监控 — 默认面板
const [hostTop, setHostTop] = useState<any[]>([]);
useEffect(() => {
(async () => {
try {
const [cpu, mem, disk] = await Promise.all([
http.get('/metrics/top?metric=cpu&limit=10'),
http.get('/metrics/top?metric=memory&limit=10'),
http.get('/metrics/top?metric=disk&limit=10'),
]);
setHostTop([
{ name: 'CPU', data: Array.isArray(cpu) ? cpu : [], key: 'cpu_usage', unit: '%', icon: 'cpu' },
{ name: '内存', data: Array.isArray(mem) ? mem : [], key: 'memory_usage', unit: '%', icon: 'mem' },
{ name: '磁盘', data: Array.isArray(disk) ? disk : [], key: 'disk_usage', unit: '%', icon: 'disk' },
]);
} catch { /* silent */ }
})();
const timer = setInterval(async () => {
try {
const cpu = await http.get('/metrics/top?metric=cpu&limit=10');
const mem = await http.get('/metrics/top?metric=memory&limit=10');
const disk = await http.get('/metrics/top?metric=disk&limit=10');
setHostTop([
{ name: 'CPU', data: Array.isArray(cpu) ? cpu : [], key: 'cpu_usage', unit: '%', icon: 'cpu' },
{ name: '内存', data: Array.isArray(mem) ? mem : [], key: 'memory_usage', unit: '%', icon: 'mem' },
{ name: '磁盘', data: Array.isArray(disk) ? disk : [], key: 'disk_usage', unit: '%', icon: 'disk' },
]);
} catch { /* silent */ }
}, 30000);
return () => clearInterval(timer);
}, []);
useEffect(() => {
fetchDashboards();
fetchDatasources();
@@ -253,6 +286,62 @@ const GrafanaDashboard: React.FC = () => {
return (
<div className={`space-y-4 ${fullscreen ? 'fixed inset-0 z-50 bg-background p-4' : ''}`}>
{/* ===== 默认:Agent 主机监控面板 ===== */}
<Card className="bg-[var(--dg-bg-secondary)] border-emerald-500/20">
<CardHeader className="pb-2">
<div className="flex items-center gap-2">
<Activity className="h-5 w-5 text-emerald-400" />
<CardTitle className="text-base text-[var(--dg-text-primary)]">Agent 主机监控</CardTitle>
<Badge variant="outline" className="text-xs bg-emerald-500/10 text-emerald-400 border-emerald-500/30">默认</Badge>
<span className="text-xs text-[var(--dg-text-muted)] ml-auto">每 30s 刷新</span>
</div>
</CardHeader>
<CardContent className="pt-0">
{hostTop.length === 0 ? (
<div className="text-center py-8 text-[var(--dg-text-muted)]">
<Server className="h-8 w-8 mx-auto mb-2 text-slate-600" />
<p className="text-sm">暂无 Agent 上报数据</p>
<p className="text-xs mt-1">请确保至少一台主机安装了 AIOps Agent 并正常上报</p>
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{hostTop.map((group: any) => (
<div key={group.name} className="bg-[var(--dg-bg-primary)] rounded-lg p-3 border border-[var(--dg-border)]">
<div className="flex items-center gap-2 mb-2">
{group.icon === 'cpu' && <Cpu className="h-4 w-4 text-red-400" />}
{group.icon === 'mem' && <Server className="h-4 w-4 text-amber-400" />}
{group.icon === 'disk' && <HardDrive className="h-4 w-4 text-orange-400" />}
<span className="text-xs font-medium text-[var(--dg-text-primary)]">{group.name} Top 10</span>
</div>
<div className="space-y-1">
{group.data.slice(0, 10).map((h: any, i: number) => {
const val = Number(h[group.key] || 0);
const barPct = Math.min(val, 100);
const barColor = val > 85 ? 'bg-red-500' : val > 60 ? 'bg-amber-500' : 'bg-emerald-500';
const dotColor = i < 3 ? (i === 0 ? 'text-red-400' : i === 1 ? 'text-amber-400' : 'text-orange-400') : 'text-slate-500';
return (
<div key={i} className="flex items-center gap-1.5 text-[11px]">
<span className={`w-4 text-right font-mono font-bold ${dotColor}`}>
{i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${i + 1}`}
</span>
<span className="w-24 truncate text-[var(--dg-text-secondary)]" title={h.hostname}>
{h.hostname}
</span>
<div className="flex-1 h-1 rounded-full bg-[var(--dg-bg-tertiary)] overflow-hidden">
<div className={`h-full rounded-full ${barColor}`} style={{ width: `${barPct}%` }} />
</div>
<span className="w-12 text-right font-mono text-[var(--dg-text-primary)]">{val.toFixed(1)}{group.unit}</span>
</div>
);
})}
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* 顶部工具栏 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">