docs: update README, API reference, dev-log, project structure with Agent monitoring + quick-alarm + latest fixes
- README.md: React 19, default pwd 123456, Docker Compose deploy, correct module list - api-reference.md: add Agent monitoring section (report/latest/rules/quick-setup/alerts/tasks/kline) - dev-log.md: new entry documenting hostname/IP fix, system collector, offline detection, quick alarm - project-structure.md: add agent/ directory, dev-log.md link, React 19
This commit is contained in:
1 parent
c51d0b8b8b
commit
4e3f8cb306
20 files changed
+1315
-1344
No files matched your search
@@ -1,356 +1,41 @@
|
||||
// Prometheus 数据源监控面板 — K8s / Node / Pod / JVM // Logan.Li
|
||||
// Prometheus 数据源面板 — 接受外部配置 // Logan.Li
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Server, Loader2, Activity, Cpu, HardDrive, Network, RefreshCw,
|
||||
Plug, Settings, X, CheckCircle2, AlertTriangle, Container,
|
||||
BarChart3, Globe, Zap, TrendingUp, TrendingDown
|
||||
} from 'lucide-react';
|
||||
import { Server, Loader2, Activity, Cpu, RefreshCw, AlertTriangle, Container, Zap, ExternalLink } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { toast } from 'sonner';
|
||||
import { http } from '@/services/http';
|
||||
|
||||
const LS_KEY = 'aiops_prometheus_ds';
|
||||
interface Props { dsUrl?: string; }
|
||||
|
||||
function getSavedDS(): { url: string } | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(LS_KEY);
|
||||
if (raw) return JSON.parse(raw);
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
function saveDS(ds: { url: string }) { localStorage.setItem(LS_KEY, JSON.stringify(ds)); }
|
||||
|
||||
// 小进度条组件
|
||||
const MiniBar = ({ val, label, color }: { val: number; label: string; color?: string }) => {
|
||||
const pct = Math.min(val, 100);
|
||||
const c = color || (val >= 80 ? 'bg-orange-500' : 'bg-purple-500');
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 min-w-[80px]">
|
||||
<div className="flex-1 h-2 rounded-full bg-[var(--dg-bg-tertiary)] overflow-hidden">
|
||||
<div className={`h-full rounded-full transition-all ${c}`} style={{ width: `${pct}%` }} />
|
||||
<div className={`h-full rounded-full ${color || 'bg-purple-500'}`} style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
<span className="w-14 text-right text-[11px] font-mono tabular-nums text-[var(--dg-text-secondary)]">{label}</span>
|
||||
<span className="w-14 text-right text-[11px] font-mono text-[var(--dg-text-secondary)]">{label}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const fmtBps = (bps: number): string => {
|
||||
if (!bps || bps === 0) return '—';
|
||||
if (bps > 1e9) return `${(bps / 1e9).toFixed(1)} GB/s`;
|
||||
if (bps > 1e6) return `${(bps / 1e6).toFixed(1)} MB/s`;
|
||||
if (bps > 1e3) return `${(bps / 1e3).toFixed(1)} KB/s`;
|
||||
return `${bps.toFixed(1)} B/s`;
|
||||
};
|
||||
|
||||
const fmtBytes = (b: number): string => {
|
||||
if (!b || b === 0) return '—';
|
||||
const fmtBytes = (b: number) => {
|
||||
if (!b) return '—';
|
||||
if (b > 1e9) return `${(b / 1e9).toFixed(1)} GB`;
|
||||
if (b > 1e6) return `${(b / 1e6).toFixed(1)} MB`;
|
||||
if (b > 1e3) return `${(b / 1e3).toFixed(1)} KB`;
|
||||
return `${b.toFixed(1)} B`;
|
||||
return `${(b / 1e3).toFixed(1)} KB`;
|
||||
};
|
||||
const fmtBps = (b: number) => {
|
||||
if (!b) return '—';
|
||||
if (b > 1e9) return `${(b / 1e9).toFixed(1)} GB/s`;
|
||||
if (b > 1e6) return `${(b / 1e6).toFixed(1)} MB/s`;
|
||||
if (b > 1e3) return `${(b / 1e3).toFixed(1)} KB/s`;
|
||||
return `${b.toFixed(1)} B/s`;
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// K8s 面板
|
||||
// ============================================
|
||||
const K8sPanel: React.FC<{ data: any }> = ({ data }) => (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<Card className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-3 text-center">
|
||||
<div className="text-2xl font-bold text-purple-400">
|
||||
{data.podCount?.[0]?.value || 0}
|
||||
</div>
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)] mt-1">Pods</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-3 text-center">
|
||||
<div className="text-2xl font-bold text-blue-400">
|
||||
{data.deployCount?.[0]?.value || 0}
|
||||
</div>
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)] mt-1">Deployments</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-3 text-center">
|
||||
<div className="text-2xl font-bold text-emerald-400">
|
||||
{data.nodeCount?.[0]?.value || 0}
|
||||
</div>
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)] mt-1">Nodes</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-3 text-center">
|
||||
<div className="text-2xl font-bold text-amber-400">
|
||||
{data.nsPods?.length || 0}
|
||||
</div>
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)] mt-1">Namespaces</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Node CPU / Memory 利用率 */}
|
||||
<div className="rounded-lg bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] p-3">
|
||||
<div className="text-xs font-medium text-[var(--dg-text-primary)] mb-2 flex items-center gap-1.5"><Cpu className="w-3 h-3 text-purple-400" />节点资源利用率</div>
|
||||
<div className="space-y-2 max-h-60 overflow-y-auto">
|
||||
{(data.nodeCPU || []).map((n: any, i: number) => {
|
||||
const mem = (data.nodeMem || []).find((m: any) => m.node === n.node);
|
||||
return (
|
||||
<div key={i} className="flex items-center gap-3 text-[11px]">
|
||||
<span className="w-28 truncate text-[var(--dg-text-secondary)]" title={n.node}>{n.node}</span>
|
||||
<MiniBar val={Number(n.value || 0)} label={`${Number(n.value || 0).toFixed(1)}%`} />
|
||||
<MiniBar val={Number(mem?.value || 0)} label={`${Number(mem?.value || 0).toFixed(1)}%`} color="bg-blue-500" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// Node 面板
|
||||
// ============================================
|
||||
const NodePanel: React.FC<{ data: any }> = ({ data }) => {
|
||||
const cpuItems = data.cpuUsage || [];
|
||||
const memItems = data.memUsage || [];
|
||||
const diskItems = data.diskUsage || [];
|
||||
const upItems = data.up || [];
|
||||
const netRx = data.netRx || [];
|
||||
const netTx = data.netTx || [];
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto rounded-lg border border-[var(--dg-border)]">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-[var(--dg-bg-tertiary)] border-b border-[var(--dg-border)]">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)]">节点</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)]">状态</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)]">CPU</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)]">内存</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)]">磁盘</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)]">流入</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)]">流出</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(data.nodes || []).map((n: any, i: number) => {
|
||||
const instance = n.instance || n.exported_instance || '';
|
||||
const cpu = cpuItems.find((c: any) => c.instance === instance);
|
||||
const mem = memItems.find((m: any) => m.instance === instance);
|
||||
const disk = diskItems.find((d: any) => d.instance === instance);
|
||||
const up = upItems.find((u: any) => u.instance === instance);
|
||||
const rx = netRx.find((r: any) => r.instance === instance);
|
||||
const tx = netTx.find((t: any) => t.instance === instance);
|
||||
const isUp = up?.value === 1;
|
||||
return (
|
||||
<tr key={i} className="border-b border-[var(--dg-border)] last:border-0 hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||
<td className="px-3 py-2 whitespace-nowrap font-medium text-[var(--dg-text-primary)]">
|
||||
{n.nodename || instance}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<Badge className={`text-[10px] ${isUp ? 'bg-emerald-500/20 text-emerald-400' : 'bg-red-500/20 text-red-400'}`}>
|
||||
{isUp ? 'UP' : 'DOWN'}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-3 py-2"><MiniBar val={Number(cpu?.value || 0)} label={`${Number(cpu?.value || 0).toFixed(1)}%`} /></td>
|
||||
<td className="px-3 py-2"><MiniBar val={Number(mem?.value || 0)} label={`${Number(mem?.value || 0).toFixed(1)}%`} color="bg-blue-500" /></td>
|
||||
<td className="px-3 py-2"><MiniBar val={Number(disk?.value || 0)} label={`${Number(disk?.value || 0).toFixed(1)}%`} color="bg-amber-500" /></td>
|
||||
<td className="px-3 py-2 font-mono text-[var(--dg-text-secondary)]">{fmtBps(Number(rx?.value || 0))}</td>
|
||||
<td className="px-3 py-2 font-mono text-[var(--dg-text-secondary)]">{fmtBps(Number(tx?.value || 0))}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// Pod 面板
|
||||
// ============================================
|
||||
const PodPanel: React.FC<{ data: any }> = ({ data }) => {
|
||||
const cpuItems = data.cpuTop || [];
|
||||
const memItems = data.memTop || [];
|
||||
const running = data.running?.[0]?.value || 0;
|
||||
const pending = data.pending?.[0]?.value || 0;
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<Card className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)]"><CardContent className="p-3 text-center"><div className="text-xl font-bold text-emerald-400">{running}</div><div className="text-[10px] text-[var(--dg-text-muted)]">Running</div></CardContent></Card>
|
||||
<Card className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)]"><CardContent className="p-3 text-center"><div className="text-xl font-bold text-amber-400">{pending}</div><div className="text-[10px] text-[var(--dg-text-muted)]">Pending</div></CardContent></Card>
|
||||
</div>
|
||||
<div className="rounded-lg bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] p-3">
|
||||
<div className="text-xs font-medium text-[var(--dg-text-primary)] mb-2">CPU Top 10</div>
|
||||
<div className="space-y-1.5 max-h-40 overflow-y-auto">
|
||||
{cpuItems.map((p: any, i: number) => (
|
||||
<div key={i} className="flex items-center gap-2 text-[11px]">
|
||||
<span className="w-10 text-right text-[var(--dg-text-muted)]">{i + 1}</span>
|
||||
<span className="w-32 truncate text-[var(--dg-text-secondary)]" title={p.pod}>{p.pod}</span>
|
||||
<MiniBar val={Number(p.value || 0) * 100} label={`${Number(p.value || 0).toFixed(2)}c`} color="bg-purple-500" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] p-3">
|
||||
<div className="text-xs font-medium text-[var(--dg-text-primary)] mb-2">内存 Top 10</div>
|
||||
<div className="space-y-1.5 max-h-40 overflow-y-auto">
|
||||
{memItems.map((p: any, i: number) => (
|
||||
<div key={i} className="flex items-center gap-2 text-[11px]">
|
||||
<span className="w-10 text-right text-[var(--dg-text-muted)]">{i + 1}</span>
|
||||
<span className="w-32 truncate text-[var(--dg-text-secondary)]" title={p.pod}>{p.pod}</span>
|
||||
<span className="ml-auto text-[var(--dg-text-primary)]">{fmtBytes(Number(p.value || 0))}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// JVM 面板
|
||||
// ============================================
|
||||
const JvmPanel: React.FC<{ data: any }> = ({ data }) => {
|
||||
const heap = Number(data.heapUsed?.[0]?.value || 0);
|
||||
const nonHeap = Number(data.nonHeapUsed?.[0]?.value || 0);
|
||||
const threads = Number(data.threadCount?.[0]?.value || 0);
|
||||
const uptime = Number(data.uptime?.[0]?.value || 0);
|
||||
const uptimeStr = uptime > 86400 ? `${(uptime / 86400).toFixed(1)}d` : `${(uptime / 3600).toFixed(1)}h`;
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-5 gap-3">
|
||||
<Card className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)]"><CardContent className="p-3 text-center"><div className="text-lg font-bold text-[var(--dg-text-primary)]">{uptimeStr}</div><div className="text-[10px] text-[var(--dg-text-muted)]">Uptime</div></CardContent></Card>
|
||||
<Card className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)]"><CardContent className="p-3 text-center"><MiniBar val={heap} label={`${heap.toFixed(1)}%`} color={heap > 80 ? 'bg-orange-500' : 'bg-purple-500'} /><div className="text-[10px] text-[var(--dg-text-muted)] mt-1">Heap</div></CardContent></Card>
|
||||
<Card className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)]"><CardContent className="p-3 text-center"><MiniBar val={nonHeap} label={`${nonHeap.toFixed(1)}%`} color="bg-blue-500" /><div className="text-[10px] text-[var(--dg-text-muted)] mt-1">Non-Heap</div></CardContent></Card>
|
||||
<Card className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)]"><CardContent className="p-3 text-center"><div className="text-lg font-bold text-emerald-400">{threads}</div><div className="text-[10px] text-[var(--dg-text-muted)]">Threads</div></CardContent></Card>
|
||||
<Card className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)]"><CardContent className="p-3 text-center"><div className="text-lg font-bold text-amber-400">{Number(data.gcPause?.[0]?.value || 0).toFixed(3)}</div><div className="text-[10px] text-[var(--dg-text-muted)]">GC Pause (s)</div></CardContent></Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 主组件
|
||||
// ============================================
|
||||
export const PrometheusDashboard: React.FC = () => {
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [url, setUrl] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [targets, setTargets] = useState(0);
|
||||
const [tab, setTab] = useState('k8s');
|
||||
|
||||
// Dashboard 数据
|
||||
const [k8sData, setK8sData] = useState<any>({});
|
||||
const [nodeData, setNodeData] = useState<any>({});
|
||||
const [podData, setPodData] = useState<any>({});
|
||||
const [jvmData, setJvmData] = useState<any>({});
|
||||
|
||||
useEffect(() => {
|
||||
const saved = getSavedDS();
|
||||
if (saved) { setUrl(saved.url); doConnect(saved.url); }
|
||||
}, []);
|
||||
|
||||
const doConnect = async (u: string) => {
|
||||
setLoading(true); setError('');
|
||||
try {
|
||||
const data: any = await http.post('/prometheus/connect', { url: u });
|
||||
setConnected(true); setTargets(data.targets || 0);
|
||||
saveDS({ url: u });
|
||||
toast.success(`Prometheus 连接成功,${data.targets} 个 targets`);
|
||||
loadAllDashboards(u);
|
||||
} catch (e: any) {
|
||||
setConnected(false); setError(e.message || '连接失败');
|
||||
} finally { setLoading(false); }
|
||||
};
|
||||
|
||||
const loadAllDashboards = async (u: string) => {
|
||||
for (const t of ['k8s', 'node', 'pod', 'jvm']) {
|
||||
try {
|
||||
const data: any = await http.post('/prometheus/dashboard', { url: u, type: t });
|
||||
const items: any = {};
|
||||
for (const [k, v] of Object.entries(data || {})) {
|
||||
items[k] = Array.isArray(v) ? v : ((v as any)?.data?.result ? parsePrune((v as any).data.result) : v);
|
||||
}
|
||||
if (t === 'k8s') setK8sData(items);
|
||||
if (t === 'node') setNodeData(items);
|
||||
if (t === 'pod') setPodData(items);
|
||||
if (t === 'jvm') setJvmData(items);
|
||||
} catch {}
|
||||
}
|
||||
};
|
||||
|
||||
const handleConnect = () => {
|
||||
if (!url) { toast.error('请输入 Prometheus URL'); return; }
|
||||
doConnect(url);
|
||||
};
|
||||
|
||||
const refresh = () => { if (url) { doConnect(url); } };
|
||||
|
||||
return (
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-purple-500/20">
|
||||
<CardHeader className="py-3 px-4 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="h-4 w-4 text-purple-400" />
|
||||
<CardTitle className="text-sm font-medium text-[var(--dg-text-primary)]">Prometheus 监控</CardTitle>
|
||||
{connected && <Badge className="text-[10px] bg-purple-500/10 text-purple-400 border-purple-500/30">{targets} targets</Badge>}
|
||||
{connected && <div className="ml-auto flex items-center gap-2">
|
||||
<Button variant="ghost" size="sm" className="h-6 px-2 text-[10px]" onClick={refresh}><RefreshCw className="w-3 h-3 mr-1" />刷新</Button>
|
||||
<Button variant="ghost" size="sm" className="h-6 px-2 text-[10px] text-red-400" onClick={() => { setConnected(false); localStorage.removeItem(LS_KEY); }}>
|
||||
<X className="w-3 h-3 mr-1" />断开
|
||||
</Button>
|
||||
</div>}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-3 pt-0">
|
||||
{loading ? (
|
||||
<div className="text-center py-12"><Loader2 className="h-6 w-6 mx-auto mb-2 animate-spin text-purple-400" /><p className="text-xs text-[var(--dg-text-muted)]">连接中...</p></div>
|
||||
) : !connected ? (
|
||||
<div>
|
||||
{error && <div className="mb-3 p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-xs text-red-400 flex items-start gap-2"><AlertTriangle className="h-4 w-4 mt-0.5" /><span>{error}</span></div>}
|
||||
{!showForm ? (
|
||||
<div className="text-center py-8 text-[var(--dg-text-muted)]">
|
||||
<Plug className="h-8 w-8 mx-auto mb-2 text-slate-600" />
|
||||
<p className="text-sm">接入 Prometheus 以显示 K8s/Node/JVM 监控</p>
|
||||
<Button size="sm" variant="outline" className="mt-4" onClick={() => setShowForm(true)}><Settings className="w-3 h-3 mr-1" />配置连接</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-w-md mx-auto p-4 space-y-3 rounded-lg bg-[var(--dg-bg-primary)] border border-[var(--dg-border)]">
|
||||
<div className="flex items-center justify-between"><span className="text-sm font-medium">Prometheus 连接</span><Button variant="ghost" size="sm" className="h-6 w-6 p-0" onClick={() => setShowForm(false)}><X className="w-3 h-3" /></Button></div>
|
||||
<div><Label className="text-xs">URL</Label><Input className="h-8 text-xs mt-1" placeholder="http://prometheus:9090" value={url} onChange={e => setUrl(e.target.value)} /></div>
|
||||
<Button size="sm" className="w-full" onClick={handleConnect}><CheckCircle2 className="w-3 h-3 mr-1" />连接</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Tabs value={tab} onValueChange={setTab}>
|
||||
<TabsList className="bg-[var(--dg-bg-tertiary)] mb-3">
|
||||
<TabsTrigger value="k8s" className="text-xs"><Container className="w-3 h-3 mr-1" />K8s 集群</TabsTrigger>
|
||||
<TabsTrigger value="node" className="text-xs"><Server className="w-3 h-3 mr-1" />节点</TabsTrigger>
|
||||
<TabsTrigger value="pod" className="text-xs"><Box className="w-3 h-3 mr-1" />Pod</TabsTrigger>
|
||||
<TabsTrigger value="jvm" className="text-xs"><Zap className="w-3 h-3 mr-1" />JVM</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="k8s"><K8sPanel data={k8sData} /></TabsContent>
|
||||
<TabsContent value="node"><NodePanel data={nodeData} /></TabsContent>
|
||||
<TabsContent value="pod"><PodPanel data={podData} /></TabsContent>
|
||||
<TabsContent value="jvm"><JvmPanel data={jvmData} /></TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// helper
|
||||
function parsePrune(result: any[]): any[] {
|
||||
return (result || []).map((r: any) => {
|
||||
const m: any = { ...(r.metric || {}) };
|
||||
@@ -359,7 +44,151 @@ function parsePrune(result: any[]): any[] {
|
||||
});
|
||||
}
|
||||
|
||||
// inline Box icon
|
||||
const Box = (props: any) => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>;
|
||||
const Box = (p: any) => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>;
|
||||
|
||||
const K8sPanel: React.FC<{ d: any }> = ({ d }) => (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<Card className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)]"><CardContent className="p-3 text-center"><div className="text-2xl font-bold text-purple-400">{d.podCount?.[0]?.value || 0}</div><div className="text-[10px] text-[var(--dg-text-muted)]">Pods</div></CardContent></Card>
|
||||
<Card className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)]"><CardContent className="p-3 text-center"><div className="text-2xl font-bold text-blue-400">{d.deployCount?.[0]?.value || 0}</div><div className="text-[10px] text-[var(--dg-text-muted)]">Deployments</div></CardContent></Card>
|
||||
<Card className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)]"><CardContent className="p-3 text-center"><div className="text-2xl font-bold text-emerald-400">{d.nodeCount?.[0]?.value || 0}</div><div className="text-[10px] text-[var(--dg-text-muted)]">Nodes</div></CardContent></Card>
|
||||
<Card className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)]"><CardContent className="p-3 text-center"><div className="text-2xl font-bold text-amber-400">{d.nsPods?.length || 0}</div><div className="text-[10px] text-[var(--dg-text-muted)]">Namespaces</div></CardContent></Card>
|
||||
</div>
|
||||
<div className="rounded-lg bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] p-3 max-h-60 overflow-y-auto">
|
||||
<div className="text-xs font-medium mb-2 flex items-center gap-1"><Cpu className="w-3 h-3 text-purple-400" />节点 CPU / 内存利用率</div>
|
||||
{(d.nodeCPU || []).map((n: any, i: number) => {
|
||||
const mem = (d.nodeMem || []).find((m: any) => m.node === n.node);
|
||||
return <div key={i} className="flex items-center gap-3 text-[11px] mb-1.5"><span className="w-28 truncate" title={n.node}>{n.node}</span><MiniBar val={Number(n.value||0)} label={`${Number(n.value||0).toFixed(1)}%`} /><MiniBar val={Number(mem?.value||0)} label={`${Number(mem?.value||0).toFixed(1)}%`} color="bg-blue-500" /></div>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const NodePanel: React.FC<{ d: any }> = ({ d }) => (
|
||||
<div className="overflow-x-auto rounded-lg border border-[var(--dg-border)]">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-[var(--dg-bg-tertiary)]"><tr><th className="px-3 py-2 text-left">节点</th><th className="px-3 py-2 text-left">状态</th><th className="px-3 py-2 text-left">CPU</th><th className="px-3 py-2 text-left">内存</th><th className="px-3 py-2 text-left">磁盘</th><th className="px-3 py-2 text-left">流入</th><th className="px-3 py-2 text-left">流出</th></tr></thead>
|
||||
<tbody>
|
||||
{(d.nodes || []).map((n: any, i: number) => {
|
||||
const inst = n.instance || n.exported_instance || '';
|
||||
const cpu = (d.cpuUsage||[]).find((x:any)=>x.instance===inst);
|
||||
const mem = (d.memUsage||[]).find((x:any)=>x.instance===inst);
|
||||
const disk = (d.diskUsage||[]).find((x:any)=>x.instance===inst);
|
||||
const up = (d.up||[]).find((x:any)=>x.instance===inst);
|
||||
const rx = (d.netRx||[]).find((x:any)=>x.instance===inst);
|
||||
const tx = (d.netTx||[]).find((x:any)=>x.instance===inst);
|
||||
return <tr key={i} className="border-b border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||
<td className="px-3 py-2 font-medium">{n.nodename||inst}</td>
|
||||
<td className="px-3 py-2"><Badge className={`text-[10px] ${up?.value===1?'bg-emerald-500/20 text-emerald-400':'bg-red-500/20 text-red-400'}`}>{up?.value===1?'UP':'DOWN'}</Badge></td>
|
||||
<td className="px-3 py-2"><MiniBar val={Number(cpu?.value||0)} label={`${Number(cpu?.value||0).toFixed(1)}%`} /></td>
|
||||
<td className="px-3 py-2"><MiniBar val={Number(mem?.value||0)} label={`${Number(mem?.value||0).toFixed(1)}%`} color="bg-blue-500" /></td>
|
||||
<td className="px-3 py-2"><MiniBar val={Number(disk?.value||0)} label={`${Number(disk?.value||0).toFixed(1)}%`} color="bg-amber-500" /></td>
|
||||
<td className="px-3 py-2 font-mono text-[var(--dg-text-secondary)]">{fmtBps(Number(rx?.value||0))}</td>
|
||||
<td className="px-3 py-2 font-mono text-[var(--dg-text-secondary)]">{fmtBps(Number(tx?.value||0))}</td>
|
||||
</tr>;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
|
||||
const PodPanel: React.FC<{ d: any }> = ({ d }) => (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<Card className="bg-[var(--dg-bg-primary)]"><CardContent className="p-3 text-center"><div className="text-xl font-bold text-emerald-400">{d.running?.[0]?.value||0}</div><div className="text-[10px] text-[var(--dg-text-muted)]">Running</div></CardContent></Card>
|
||||
<Card className="bg-[var(--dg-bg-primary)]"><CardContent className="p-3 text-center"><div className="text-xl font-bold text-amber-400">{d.pending?.[0]?.value||0}</div><div className="text-[10px] text-[var(--dg-text-muted)]">Pending</div></CardContent></Card>
|
||||
</div>
|
||||
<div className="rounded-lg bg-[var(--dg-bg-primary)] p-3 max-h-40 overflow-y-auto"><div className="text-xs font-medium mb-2">CPU Top 10</div>{(d.cpuTop||[]).map((p:any,i:number)=><div key={i} className="flex items-center gap-2 text-[11px] mb-1"><span className="w-10 text-right text-[var(--dg-text-muted)]">{i+1}</span><span className="w-32 truncate" title={p.pod}>{p.pod}</span><MiniBar val={Number(p.value||0)*100} label={`${Number(p.value||0).toFixed(2)}c`} color="bg-purple-500" /></div>)}</div>
|
||||
<div className="rounded-lg bg-[var(--dg-bg-primary)] p-3 max-h-40 overflow-y-auto"><div className="text-xs font-medium mb-2">内存 Top 10</div>{(d.memTop||[]).map((p:any,i:number)=><div key={i} className="flex items-center gap-2 text-[11px] mb-1"><span className="w-10 text-right text-[var(--dg-text-muted)]">{i+1}</span><span className="w-32 truncate" title={p.pod}>{p.pod}</span><span className="ml-auto">{fmtBytes(Number(p.value||0))}</span></div>)}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const JvmPanel: React.FC<{ d: any }> = ({ d }) => {
|
||||
const h = Number(d.heapUsed?.[0]?.value||0);
|
||||
const uptime = Number(d.uptime?.[0]?.value||0);
|
||||
return (
|
||||
<div className="grid grid-cols-5 gap-3">
|
||||
<Card className="bg-[var(--dg-bg-primary)]"><CardContent className="p-3 text-center"><div className="text-lg font-bold">{uptime>86400?`${(uptime/86400).toFixed(1)}d`:`${(uptime/3600).toFixed(1)}h`}</div><div className="text-[10px] text-[var(--dg-text-muted)]">Uptime</div></CardContent></Card>
|
||||
<Card className="bg-[var(--dg-bg-primary)]"><CardContent className="p-3 text-center"><MiniBar val={h} label={`${h.toFixed(1)}%`} color={h>80?'bg-orange-500':'bg-purple-500'} /><div className="text-[10px] text-[var(--dg-text-muted)] mt-1">Heap</div></CardContent></Card>
|
||||
<Card className="bg-[var(--dg-bg-primary)]"><CardContent className="p-3 text-center"><MiniBar val={Number(d.nonHeapUsed?.[0]?.value||0)} label={`${Number(d.nonHeapUsed?.[0]?.value||0).toFixed(1)}%`} color="bg-blue-500" /><div className="text-[10px] text-[var(--dg-text-muted)] mt-1">Non-Heap</div></CardContent></Card>
|
||||
<Card className="bg-[var(--dg-bg-primary)]"><CardContent className="p-3 text-center"><div className="text-lg font-bold text-emerald-400">{d.threadCount?.[0]?.value||0}</div><div className="text-[10px] text-[var(--dg-text-muted)]">Threads</div></CardContent></Card>
|
||||
<Card className="bg-[var(--dg-bg-primary)]"><CardContent className="p-3 text-center"><div className="text-lg font-bold text-amber-400">{Number(d.gcPause?.[0]?.value||0).toFixed(3)}</div><div className="text-[10px] text-[var(--dg-text-muted)]">GC Pause (s)</div></CardContent></Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const PrometheusDashboard: React.FC<Props> = ({ dsUrl }) => {
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [targets, setTargets] = useState(0);
|
||||
const [error, setError] = useState('');
|
||||
const [tab, setTab] = useState('k8s');
|
||||
const [k8sD, setK8sD] = useState<any>({});
|
||||
const [nodeD, setNodeD] = useState<any>({});
|
||||
const [podD, setPodD] = useState<any>({});
|
||||
const [jvmD, setJvmD] = useState<any>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (dsUrl) doConnect(dsUrl);
|
||||
}, [dsUrl]);
|
||||
|
||||
const doConnect = async (u: string) => {
|
||||
setLoading(true); setError('');
|
||||
try {
|
||||
const data: any = await http.post('/prometheus/connect', { url: u });
|
||||
setConnected(true); setTargets(data.targets||0);
|
||||
loadAll(u);
|
||||
} catch (e: any) { setConnected(false); setError(e.message||'连接失败'); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
|
||||
const loadAll = async (u: string) => {
|
||||
for (const t of ['k8s','node','pod','jvm']) {
|
||||
try {
|
||||
const data: any = await http.post('/prometheus/dashboard', { url: u, type: t });
|
||||
const items: any = {};
|
||||
for (const [k, v] of Object.entries(data||{}))
|
||||
items[k] = Array.isArray(v) ? v : ((v as any)?.data?.result ? parsePrune((v as any).data.result) : v);
|
||||
if (t==='k8s') setK8sD(items); if (t==='node') setNodeD(items);
|
||||
if (t==='pod') setPodD(items); if (t==='jvm') setJvmD(items);
|
||||
} catch {}
|
||||
}
|
||||
};
|
||||
|
||||
const refresh = () => { if (dsUrl) doConnect(dsUrl); };
|
||||
|
||||
if (!connected && !error) return null;
|
||||
|
||||
return (
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-purple-500/20">
|
||||
<CardHeader className="py-3 px-4 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="h-4 w-4 text-purple-400" />
|
||||
<CardTitle className="text-sm font-medium">Prometheus 监控</CardTitle>
|
||||
{connected && <Badge className="text-[10px] bg-purple-500/10 text-purple-400">{targets} targets</Badge>}
|
||||
{connected && <div className="ml-auto"><Button variant="ghost" size="sm" className="h-6 px-2 text-[10px]" onClick={refresh}><RefreshCw className="w-3 h-3 mr-1" />刷新</Button></div>}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-3 pt-0">
|
||||
{loading ? (<div className="text-center py-12"><Loader2 className="h-6 w-6 mx-auto mb-2 animate-spin text-purple-400" /><p className="text-xs text-[var(--dg-text-muted)]">连接中…</p></div>)
|
||||
: error ? (<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-xs text-red-400"><AlertTriangle className="h-4 w-4 inline mr-1" />{error}</div>)
|
||||
: (
|
||||
<Tabs value={tab} onValueChange={setTab}>
|
||||
<TabsList className="bg-[var(--dg-bg-tertiary)] mb-3">
|
||||
<TabsTrigger value="k8s" className="text-xs"><Container className="w-3 h-3 mr-1" />K8s</TabsTrigger>
|
||||
<TabsTrigger value="node" className="text-xs"><Server className="w-3 h-3 mr-1" />Node</TabsTrigger>
|
||||
<TabsTrigger value="pod" className="text-xs"><Box className="w-3 h-3 mr-1" />Pod</TabsTrigger>
|
||||
<TabsTrigger value="jvm" className="text-xs"><Zap className="w-3 h-3 mr-1" />JVM</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="k8s"><K8sPanel d={k8sD} /></TabsContent>
|
||||
<TabsContent value="node"><NodePanel d={nodeD} /></TabsContent>
|
||||
<TabsContent value="pod"><PodPanel d={podD} /></TabsContent>
|
||||
<TabsContent value="jvm"><JvmPanel d={jvmD} /></TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrometheusDashboard;
|
||||
Reference in new issue
Block a user