- 新增 PageShell/PageTabs/ContentPanel/PageLoading/PageEmpty 页面骨架 - index.css 设计令牌文档化,布局令牌与纯色主按钮(去除渐变粉彩) - 全站路由页接入 PageShell,云账单三 Tab 为标杆 - 主题体系:跟随系统 + 主流/开发者分组,新增 Dracula/Tokyo Night,One Dark 暖灰差异化 - 登录页/侧栏/顶栏去渐变光球,企业克制风 - 修复 AIAlertAnalyzer JSX 与构建依赖导入问题
200 lines
12 KiB
TypeScript
200 lines
12 KiB
TypeScript
// Prometheus 数据源面板 — 接受外部配置 // Logan.Li
|
|
import React, { useState, useEffect } from 'react';
|
|
import { Server, Loader2, Activity, Cpu, RefreshCw, AlertTriangle, Container, Zap} from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { StatStrip } from '@/components/ui/stat-strip';
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
import { prometheusProxyApi } from '@/api';
|
|
import type { JsonData } from '@/types/common';
|
|
|
|
interface Props { dsUrl?: string; }
|
|
|
|
const MiniBar = ({ val, label, color }: { val: number; label: string; color?: string }) => {
|
|
const pct = Math.min(val, 100);
|
|
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 ${color || 'bg-purple-500'}`} style={{ width: `${pct}%` }} />
|
|
</div>
|
|
<span className="w-14 text-right text-[11px] font-mono text-[var(--dg-text-secondary)]">{label}</span>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
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`;
|
|
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`;
|
|
};
|
|
|
|
function parsePrune( result: JsonData[]): JsonData[] {
|
|
return (result || []).map((r: JsonData) => {
|
|
const m: JsonData = { ...(r.metric || {}) };
|
|
if (r.value) m.value = r.value[1];
|
|
return m;
|
|
});
|
|
}
|
|
|
|
const Box = (p: JsonData) => <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: JsonData}> = ({ d }) => (
|
|
<div className="space-y-4">
|
|
<StatStrip
|
|
items={[
|
|
{ label: 'Pods', value: d.podCount?.[0]?.value || 0, color: 'purple' },
|
|
{ label: 'Deployments', value: d.deployCount?.[0]?.value || 0, color: 'blue' },
|
|
{ label: 'Nodes', value: d.nodeCount?.[0]?.value || 0, color: 'emerald' },
|
|
{ label: 'Namespaces', value: d.nsPods?.length || 0, color: 'amber' },
|
|
]}
|
|
/>
|
|
<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 dg-text-purple" />节点 CPU / 内存利用率</div>
|
|
{(d.nodeCPU || []).map((n: JsonData, i: number) => {
|
|
const mem = (d.nodeMem || []).find((m: JsonData) => 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: JsonData}> = ({ 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: JsonData, i: number) => {
|
|
const inst = n.instance || n.exported_instance || '';
|
|
const cpu = (d.cpuUsage||[]).find((x: JsonData)=>x.instance===inst);
|
|
const mem = (d.memUsage||[]).find((x: JsonData)=>x.instance===inst);
|
|
const disk = (d.diskUsage||[]).find((x: JsonData)=>x.instance===inst);
|
|
const up = (d.up||[]).find((x: JsonData)=>x.instance===inst);
|
|
const rx = (d.netRx||[]).find((x: JsonData)=>x.instance===inst);
|
|
const tx = (d.netTx||[]).find((x: JsonData)=>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 dg-text-green':'bg-red-500/20 dg-text-red'}`}>{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: JsonData}> = ({ d }) => (
|
|
<div className="space-y-4">
|
|
<StatStrip
|
|
items={[
|
|
{ label: 'Running', value: d.running?.[0]?.value || 0, color: 'emerald' },
|
|
{ label: 'Pending', value: d.pending?.[0]?.value || 0, color: 'amber' },
|
|
]}
|
|
/>
|
|
<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: JsonData, 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: JsonData, 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: JsonData}> = ({ 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 dg-text-green">{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 dg-text-yellow">{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('kubernetes');
|
|
const [k8sD, setK8sD] = useState<JsonData>({});
|
|
const [nodeD, setNodeD] = useState<JsonData>({});
|
|
const [podD, setPodD] = useState<JsonData>({});
|
|
const [jvmD, setJvmD] = useState<JsonData>({});
|
|
|
|
useEffect(() => {
|
|
if (dsUrl) doConnect(dsUrl);
|
|
}, [dsUrl]);
|
|
|
|
const doConnect = async (u: string) => {
|
|
setLoading(true); setError('');
|
|
try {
|
|
const data: JsonData = await prometheusProxyApi.connect(u);
|
|
setConnected(true); setTargets(data.targets||0);
|
|
loadAll(u);
|
|
} catch (e) { setConnected(false); setError((e as JsonData).message||'连接失败'); }
|
|
finally { setLoading(false); }
|
|
};
|
|
|
|
const loadAll = async (u: string) => {
|
|
for (const t of ['kubernetes','node','pod','jvm']) {
|
|
try {
|
|
const data: JsonData = await prometheusProxyApi.dashboard(u, t);
|
|
const items: JsonData = {};
|
|
for (const [k, v] of Object.entries(data||{}))
|
|
items[k] = Array.isArray(v) ? v : ((v as JsonData)?.data?.result ? parsePrune((v as JsonData).data.result) : v);
|
|
if (t==='kubernetes') 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 dg-text-purple" />
|
|
<CardTitle className="text-sm font-medium">Prometheus 监控</CardTitle>
|
|
{connected && <Badge className="text-[10px] bg-purple-500/10 dg-text-purple">{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 dg-text-purple" /><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 dg-text-red"><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="kubernetes" className="text-xs"><Container className="w-3 h-3 mr-1" />Kubernetes</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="kubernetes"><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;
|