snapshot: 数据源管理+大屏增强前快照
This commit is contained in:
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;
|
||||
Reference in new issue
Block a user