snapshot: 数据源管理+大屏增强前快照

This commit is contained in:
attacker committed 2026-06-18 14:01:22 +00:00
1 parent 2bf3e7c308
commit 1726ad65ac
19 files changed
+2540 -273

No files matched your search

@@ -0,0 +1,310 @@
// Zabbix 数据源监控面板 — 镜像 Grafana Dashboard 5456 + 5363 // Logan.Li
import React, { useState, useEffect } from 'react';
import {
Server, Wifi, WifiOff, Loader2, AlertTriangle, Activity,
Cpu, HardDrive, Network, Clock, RefreshCw, Plug, Settings,
ArrowUpDown, ArrowUp, ArrowDown, X, CheckCircle2
} 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 { toast } from 'sonner';
import { http } from '@/services/http';
interface ZabbixHost {
hostid: string;
hostname: string;
name: string;
ip: string;
available: string; // 0=未知 1=可用 2=不可用
cpu_usage: number;
memory_usage: number;
disk_usage: number;
load5: number;
net_in_bps: number;
net_out_bps: number;
problem_count: number;
uptime_days: number;
cpu_count: number;
total_memory_gb: number;
}
interface ZabbixData {
version: string;
hosts: ZabbixHost[];
total: number;
}
type SortField = 'hostname' | 'cpu_usage' | 'memory_usage' | 'disk_usage' | 'load5' | 'net_in_bps' | 'net_out_bps' | 'problem_count' | 'uptime_days' | 'ip';
const LS_KEY = 'aiops_zabbix_ds';
function getSavedDS(): { url: string; username: string; password: string } | null {
try {
const raw = localStorage.getItem(LS_KEY);
if (raw) return JSON.parse(raw);
} catch {}
return null;
}
function saveDS(ds: { url: string; username: string; password: string }) {
localStorage.setItem(LS_KEY, JSON.stringify(ds));
}
export const ZabbixDashboard: React.FC = () => {
const [connected, setConnected] = useState(false);
const [loading, setLoading] = useState(false);
const [hosts, setHosts] = useState<ZabbixHost[]>([]);
const [version, setVersion] = useState('');
const [error, setError] = useState('');
const [url, setUrl] = useState('');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [showForm, setShowForm] = useState(false);
const [sortField, setSortField] = useState<SortField>('cpu_usage');
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc');
// 尝试用保存的凭据自动连接
useEffect(() => {
const saved = getSavedDS();
if (saved) {
setUrl(saved.url);
setUsername(saved.username);
setPassword(saved.password);
doConnect(saved.url, saved.username, saved.password);
}
}, []);
const doConnect = async (u: string, un: string, pw: string) => {
setLoading(true);
setError('');
try {
const data: ZabbixData = await http.post('/zabbix/connect', { url: u, username: un, password: pw });
setHosts(data.hosts || []);
setVersion(data.version || '');
setConnected(true);
saveDS({ url: u, username: un, password: pw });
toast.success(`Zabbix ${data.version} 连接成功,${data.total} 台主机`);
} catch (e: any) {
setConnected(false);
setError(e.message || '连接失败');
} finally {
setLoading(false);
}
};
const handleConnect = () => {
if (!url || !username || !password) {
toast.error('请填写完整连接信息');
return;
}
doConnect(url, username, password);
};
const handleDisconnect = () => {
setConnected(false);
setHosts([]);
setError('');
localStorage.removeItem(LS_KEY);
toast.info('已断开 Zabbix 连接');
};
const refreshData = () => {
if (url && username && password) doConnect(url, username, password);
};
const toggleSort = (field: SortField) => {
if (sortField === field) setSortDir(d => d === 'asc' ? 'desc' : 'asc');
else { setSortField(field); setSortDir('desc'); }
};
const sortedHosts = [...hosts].sort((a, b) => {
const va = (a as any)[sortField] ?? '';
const vb = (b as any)[sortField] ?? '';
if (typeof va === 'number' && typeof vb === 'number') {
return sortDir === 'asc' ? va - vb : vb - va;
}
return sortDir === 'asc' ? String(va).localeCompare(String(vb)) : String(vb).localeCompare(String(va));
});
const SortIcon = ({ field }: { field: SortField }) => {
if (sortField !== field) return <ArrowUpDown className="w-3 h-3 text-slate-500" />;
return sortDir === 'asc' ? <ArrowUp className="w-3 h-3 text-blue-400" /> : <ArrowDown className="w-3 h-3 text-blue-400" />;
};
const usageBar = (val: number, label: string) => {
const pct = Math.min(val, 100);
const isWarn = val >= 80;
return (
<div className="flex items-center gap-2 min-w-[90px]">
<div className="flex-1 h-2.5 rounded-full bg-[var(--dg-bg-tertiary)] overflow-hidden">
<div className={`h-full rounded-full transition-all duration-500 ${isWarn ? 'bg-orange-500' : 'bg-blue-500'}`}
style={{ width: `${pct}%` }} />
</div>
<span className={`w-14 text-right text-xs font-mono tabular-nums ${isWarn ? 'text-orange-400' : ''}`}>{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 getAvailBadge = (avail: string) => {
return avail === '1'
? <Badge className="text-[10px] bg-emerald-500/20 text-emerald-400 border-emerald-500/30"><Wifi className="w-3 h-3 mr-1" />可用</Badge>
: <Badge className="text-[10px] bg-red-500/20 text-red-400 border-red-500/30"><WifiOff className="w-3 h-3 mr-1" />不可用</Badge>;
};
const getPriorityColor = (count: number) => {
if (count === 0) return '';
if (count <= 2) return 'text-amber-400';
return 'text-red-400';
};
return (
<Card className="bg-[var(--dg-bg-secondary)] border-blue-500/20">
<CardHeader className="py-3 px-4 pb-2">
<div className="flex items-center gap-2">
<Activity className="h-4 w-4 text-blue-400" />
<CardTitle className="text-sm font-medium text-[var(--dg-text-primary)]">Zabbix 主机监控</CardTitle>
{version && <Badge className="text-[10px] bg-blue-500/10 text-blue-400 border-blue-500/30">v{version}</Badge>}
{connected && (
<>
<span className="text-[10px] text-[var(--dg-text-muted)] ml-2">{hosts.length} 台主机</span>
<div className="ml-auto flex items-center gap-2">
<Button variant="ghost" size="sm" className="h-6 px-2 text-[10px]" onClick={refreshData}><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={handleDisconnect}><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 text-[var(--dg-text-muted)]">
<Loader2 className="h-6 w-6 mx-auto mb-2 animate-spin text-blue-400" />
<p className="text-xs">正在连接 Zabbix...</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 flex-shrink-0" />
<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">接入 Zabbix 数据源以显示主机监控</p>
<p className="text-xs mt-1 mb-4">支持 Zabbix 5.0 / 6.0 / 7.0</p>
<div className="flex gap-2 justify-center">
<Button size="sm" variant="outline" onClick={() => setShowForm(true)}>
<Settings className="w-3 h-3 mr-1" />配置连接
</Button>
</div>
</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 text-[var(--dg-text-primary)]">Zabbix 连接配置</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">API URL</Label>
<Input className="h-8 text-xs mt-1" placeholder="http://zabbix.example.com/api_jsonrpc.php"
value={url} onChange={e => setUrl(e.target.value)} />
</div>
<div>
<Label className="text-xs">用户名</Label>
<Input className="h-8 text-xs mt-1" placeholder="Admin" value={username}
onChange={e => setUsername(e.target.value)} />
</div>
<div>
<Label className="text-xs">密码</Label>
<Input className="h-8 text-xs mt-1" type="password" placeholder="••••••••" value={password}
onChange={e => setPassword(e.target.value)} />
</div>
<Button size="sm" className="w-full" onClick={handleConnect}>
<CheckCircle2 className="w-3 h-3 mr-1" />连接 Zabbix
</Button>
</div>
)}
</div>
) : hosts.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">未找到被监控的 Zabbix 主机</p>
</div>
) : (
<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>
{[
{ field: 'hostname' as SortField, label: '主机名', w: '' },
{ field: 'ip' as SortField, label: 'IP', w: '' },
{ field: 'cpu_usage' as SortField, label: 'CPU', w: 'min-w-[90px]' },
{ field: 'memory_usage' as SortField, label: '内存', w: 'min-w-[90px]' },
{ field: 'disk_usage' as SortField, label: '磁盘', w: 'min-w-[90px]' },
{ field: 'load5' as SortField, label: 'Load5', w: '' },
{ field: 'net_in_bps' as SortField, label: '流入', w: '' },
{ field: 'net_out_bps' as SortField, label: '流出', w: '' },
{ field: 'problem_count' as SortField, label: '问题', w: '' },
{ field: 'uptime_days' as SortField, label: '运行', w: '' },
].map(({ field, label, w }) => (
<th key={field}
className={`px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)] cursor-pointer hover:text-[var(--dg-text-primary)] select-none whitespace-nowrap ${w}`}
onClick={() => toggleSort(field)}>
<div className="flex items-center gap-1">{label}<SortIcon field={field} /></div>
</th>
))}
</tr>
</thead>
<tbody>
{sortedHosts.map((host, i) => (
<tr key={host.hostid || i} className="border-b border-[var(--dg-border)] last:border-0 hover:bg-[var(--dg-bg-tertiary)]/50 transition-colors">
<td className="px-3 py-2.5 whitespace-nowrap">
<div className="flex items-center gap-1.5">
{getAvailBadge(host.available)}
<span className="font-medium text-[var(--dg-text-primary)]">{host.name || host.hostname}</span>
</div>
</td>
<td className="px-3 py-2.5 whitespace-nowrap font-mono text-[var(--dg-text-secondary)]">{host.ip || '—'}</td>
<td className="px-3 py-2.5">{usageBar(Number(host.cpu_usage || 0), `${Number(host.cpu_usage || 0).toFixed(1)}%`)}</td>
<td className="px-3 py-2.5">{usageBar(Number(host.memory_usage || 0), `${Number(host.memory_usage || 0).toFixed(1)}%`)}</td>
<td className="px-3 py-2.5">{usageBar(Number(host.disk_usage || 0), `${Number(host.disk_usage || 0).toFixed(1)}%`)}</td>
<td className="px-3 py-2.5 whitespace-nowrap text-[var(--dg-text-secondary)]">{host.load5 > 0 ? host.load5.toFixed(1) : '—'}</td>
<td className="px-3 py-2.5 whitespace-nowrap font-mono text-[var(--dg-text-secondary)]">{fmtBps(host.net_in_bps)}</td>
<td className="px-3 py-2.5 whitespace-nowrap font-mono text-[var(--dg-text-secondary)]">{fmtBps(host.net_out_bps)}</td>
<td className="px-3 py-2.5 whitespace-nowrap text-center">
<span className={`font-mono font-bold ${getPriorityColor(host.problem_count)}`}>
{host.problem_count > 0
? <span className="inline-flex items-center gap-1"><AlertTriangle className="w-3 h-3" />{host.problem_count}</span>
: <span className="text-emerald-400">0</span>}
</span>
</td>
<td className="px-3 py-2.5 whitespace-nowrap text-[var(--dg-text-secondary)]">
{host.uptime_days > 0 ? `${host.uptime_days}d` : '—'}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
);
};
export default ZabbixDashboard;