feat: 全站 PageShell 设计系统与专业 UI 统一
- 新增 PageShell/PageTabs/ContentPanel/PageLoading/PageEmpty 页面骨架 - index.css 设计令牌文档化,布局令牌与纯色主按钮(去除渐变粉彩) - 全站路由页接入 PageShell,云账单三 Tab 为标杆 - 主题体系:跟随系统 + 主流/开发者分组,新增 Dracula/Tokyo Night,One Dark 暖灰差异化 - 登录页/侧栏/顶栏去渐变光球,企业克制风 - 修复 AIAlertAnalyzer JSX 与构建依赖导入问题
This commit is contained in:
1 parent
1acb0c49af
commit
e363ded445
213 files changed
+7148
-6818
No files matched your search
@@ -46,6 +46,9 @@ import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { PageShell } from '@/components/layout/PageShell';
|
||||
import { PageLoading } from '@/components/layout/PageLoading';
|
||||
import { PageEmpty } from '@/components/layout/PageEmpty';
|
||||
|
||||
// ═══════════ Tab 配置 ═══════════
|
||||
type TabId = 'agent' | 'zabbix' | 'prometheus' | 'dashboard';
|
||||
@@ -129,9 +132,9 @@ const AgentTab: React.FC = () => {
|
||||
return sd === 'asc' ? String(va).localeCompare(String(vb)) : String(vb).localeCompare(String(va));
|
||||
});
|
||||
const paged = sorted.slice((page - 1) * pageSize, page * pageSize);
|
||||
const SI = ({ f }: { f: SortField }) => sf !== f ? <ArrowUpDown className="w-3 h-3 text-slate-500" /> : sd === 'asc' ? <ArrowUp className="w-3 h-3 text-emerald-400" /> : <ArrowDown className="w-3 h-3 text-emerald-400" />;
|
||||
const SI = ({ f }: { f: SortField }) => sf !== f ? <ArrowUpDown className="w-3 h-3 dg-text-muted" /> : sd === 'asc' ? <ArrowUp className="w-3 h-3 dg-text-green" /> : <ArrowDown className="w-3 h-3 dg-text-green" />;
|
||||
const bar = (v: number, l: string) => (
|
||||
<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 ${v >= 80 ? 'bg-orange-500' : 'bg-emerald-500'}`} style={{ width: `${Math.min(v, 100)}%` }} /></div><span className={`w-14 text-right text-xs font-mono tabular-nums ${v >= 80 ? 'text-orange-400' : ''}`}>{l}</span></div>
|
||||
<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 ${v >= 80 ? 'bg-orange-500' : 'bg-emerald-500'}`} style={{ width: `${Math.min(v, 100)}%` }} /></div><span className={`w-14 text-right text-xs font-mono tabular-nums ${v >= 80 ? 'dg-text-orange' : ''}`}>{l}</span></div>
|
||||
);
|
||||
|
||||
const selAll = selected.size === hosts.length && hosts.length > 0;
|
||||
@@ -199,8 +202,14 @@ const AgentTab: React.FC = () => {
|
||||
finally { setBatchSubmitting(false); }
|
||||
};
|
||||
|
||||
if (loading) return <div className="text-center py-20 text-[var(--dg-text-muted)]"><Activity className="h-6 w-6 mx-auto mb-2 animate-spin" /><p className="text-xs">加载中…</p></div>;
|
||||
if (!hosts.length) return <div className="text-center py-20 text-[var(--dg-text-muted)]"><Server className="h-8 w-8 mx-auto mb-2" /><p className="text-sm">暂无服务器资产</p><p className="text-xs mt-1">请先在「服务器管理」中添加主机,然后在此安装 Agent</p></div>;
|
||||
if (loading) return <PageLoading label="加载 Agent 主机列表..." />;
|
||||
if (!hosts.length) return (
|
||||
<PageEmpty
|
||||
icon={<Server className="h-8 w-8" />}
|
||||
title="暂无服务器资产"
|
||||
description="请先在「服务器管理」中添加主机,然后在此安装 Agent"
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
{/* 工具栏 */}
|
||||
@@ -216,10 +225,10 @@ const AgentTab: React.FC = () => {
|
||||
</Button>
|
||||
{selAny && (
|
||||
<>
|
||||
<Button size="sm" variant="outline" className="h-7 text-xs gap-1 border-blue-500/30 text-blue-400 hover:bg-blue-500/10" onClick={openBatchDialog}>
|
||||
<Button size="sm" variant="outline" className="h-7 text-xs gap-1 dg-btn-secondary dg-text-blue" onClick={openBatchDialog}>
|
||||
<PackageOpen className="w-3 h-3" />批量部署 ({selected.size})
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="h-7 text-xs gap-1 border-red-500/30 text-red-400 hover:bg-red-500/10" onClick={handleBatchUninstall} disabled={batchUninstalling}>
|
||||
<Button size="sm" variant="outline" className="h-7 text-xs gap-1 border-red-500/30 dg-text-red hover:bg-red-500/10" onClick={handleBatchUninstall} disabled={batchUninstalling}>
|
||||
<Trash2 className="w-3 h-3" />批量卸载 ({selected.size})
|
||||
</Button>
|
||||
</>
|
||||
@@ -236,27 +245,27 @@ const AgentTab: React.FC = () => {
|
||||
|
||||
{/* ────────── 批量部署 Agent 对话框 ────────── */}
|
||||
<Dialog open={batchOpen} onOpenChange={(v) => { if (!v) { setBatchOpen(false); setBatchAck(false); setBatchCreds({ username: 'root', password: '', sshKey: '', authType: 'password' }); } }}>
|
||||
<DialogContent className="max-w-2xl max-h-[85vh] overflow-auto bg-slate-900 border-slate-700 text-slate-100">
|
||||
<DialogContent className="max-w-2xl max-h-[85vh] overflow-auto dg-input">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-base">
|
||||
<PackageOpen className="w-5 h-5 text-blue-400" />
|
||||
<PackageOpen className="w-5 h-5 dg-text-blue" />
|
||||
批量部署 Agent
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-400 text-xs">
|
||||
<DialogDescription className="dg-text-secondary text-xs">
|
||||
选中 {selectedHosts.length} 台主机(需安装: {needInstall.length},待升级: {needUpgrade.length})
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* 模式选择 — 类似阿里云安装/升级切换 */}
|
||||
<div className="flex items-center gap-2 p-1 rounded-lg bg-slate-800 border border-slate-700 w-fit">
|
||||
<div className="flex items-center gap-2 p-1 rounded-lg bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] w-fit">
|
||||
{(['install', 'upgrade'] as const).map(m => (
|
||||
<button
|
||||
key={m}
|
||||
disabled={m === 'install' ? needInstall.length === 0 : needUpgrade.length === 0}
|
||||
onClick={() => setBatchMode(m)}
|
||||
className={`px-4 py-1.5 rounded text-xs font-medium transition-colors disabled:opacity-30 disabled:cursor-not-allowed ${
|
||||
batchMode === m ? 'bg-blue-600 text-white shadow-sm' : 'text-slate-400 hover:text-slate-200'
|
||||
batchMode === m ? 'dg-btn-primary text-white shadow-sm' : 'dg-text-secondary hover:text-[var(--dg-text-primary)]'
|
||||
}`}
|
||||
>
|
||||
{m === 'install' ? `⚡ 安装 (${needInstall.length})` : `🔄 升级到最新版 (${needUpgrade.length})`}
|
||||
@@ -265,32 +274,32 @@ const AgentTab: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* 主机列表 */}
|
||||
<div className="rounded-md border border-slate-700 overflow-hidden">
|
||||
<div className="rounded-md border border-[var(--dg-border)] overflow-hidden">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-slate-800">
|
||||
<thead className="bg-[var(--dg-bg-tertiary)]">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-medium text-slate-400">主机名</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-slate-400">IP</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-slate-400">状态</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-slate-400">当前版本</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-slate-400">操作</th>
|
||||
<th className="px-3 py-2 text-left font-medium dg-text-secondary">主机名</th>
|
||||
<th className="px-3 py-2 text-left font-medium dg-text-secondary">IP</th>
|
||||
<th className="px-3 py-2 text-left font-medium dg-text-secondary">状态</th>
|
||||
<th className="px-3 py-2 text-left font-medium dg-text-secondary">当前版本</th>
|
||||
<th className="px-3 py-2 text-left font-medium dg-text-secondary">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(batchMode === 'install' ? needInstall : needUpgrade).map(h => (
|
||||
<tr key={h.server_id} className="border-t border-slate-800 hover:bg-slate-800/50">
|
||||
<td className="px-3 py-2 font-medium text-slate-200">{h.hostname}</td>
|
||||
<td className="px-3 py-2 font-mono text-slate-400">{h.ip}</td>
|
||||
<tr key={h.server_id} className="border-t border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]">
|
||||
<td className="px-3 py-2 font-medium text-[var(--dg-text-primary)]">{h.hostname}</td>
|
||||
<td className="px-3 py-2 font-mono dg-text-secondary">{h.ip}</td>
|
||||
<td className="px-3 py-2">
|
||||
{h.agent_status === 'online'
|
||||
? <Badge className="text-[10px] bg-emerald-500/20 text-emerald-400">在线</Badge>
|
||||
: <Badge className="text-[10px] bg-slate-500/20 text-slate-400">
|
||||
? <Badge className="text-[10px] bg-emerald-500/20 dg-text-green">在线</Badge>
|
||||
: <Badge className="text-[10px] bg-[var(--dg-bg-active)]/20 dg-text-secondary">
|
||||
{h.agent_status === 'not_installed' ? '未安装' : h.agent_status || '离线'}
|
||||
</Badge>}
|
||||
</td>
|
||||
<td className="px-3 py-2 font-mono text-slate-400">{h.agent_version || '—'}</td>
|
||||
<td className="px-3 py-2 font-mono dg-text-secondary">{h.agent_version || '—'}</td>
|
||||
<td className="px-3 py-2">
|
||||
<span className="text-blue-400 text-[11px]">
|
||||
<span className="dg-text-blue text-[11px]">
|
||||
{batchMode === 'install' ? '即将安装' : '即将升级'}
|
||||
</span>
|
||||
</td>
|
||||
@@ -302,38 +311,38 @@ const AgentTab: React.FC = () => {
|
||||
|
||||
{/* 安装凭据(安装模式且存在未保存凭据的主机时显示)// Logan.Li */}
|
||||
{batchMode === 'install' && needInstall.some(h => !h.has_auth) && (
|
||||
<div className="rounded-md border border-slate-700 bg-slate-800/50 p-3 space-y-3">
|
||||
<div className="rounded-md border border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]/50 p-3 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Info className="w-4 h-4 text-blue-400 shrink-0" />
|
||||
<span className="text-xs text-slate-300">以下主机未保存 SSH 凭据,请提供临时凭据用于批量安装(仅本次使用):</span>
|
||||
<Info className="w-4 h-4 dg-text-blue shrink-0" />
|
||||
<span className="text-xs text-[var(--dg-text-secondary)]">以下主机未保存 SSH 凭据,请提供临时凭据用于批量安装(仅本次使用):</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-[11px] text-slate-400 mb-1 block">认证方式</Label>
|
||||
<Label className="text-[11px] dg-text-secondary mb-1 block">认证方式</Label>
|
||||
<Select value={batchCreds.authType} onValueChange={(v: 'password' | 'key') => setBatchCreds(c => ({ ...c, authType: v }))}>
|
||||
<SelectTrigger className="h-8 text-xs bg-slate-900 border-slate-700 text-slate-200">
|
||||
<SelectTrigger className="h-8 text-xs dg-input text-[var(--dg-text-primary)]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-900 border-slate-700 text-slate-200">
|
||||
<SelectContent className="dg-input text-[var(--dg-text-primary)]">
|
||||
<SelectItem value="password">密码</SelectItem>
|
||||
<SelectItem value="key">SSH 私钥</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[11px] text-slate-400 mb-1 block">用户名</Label>
|
||||
<Input value={batchCreds.username} onChange={e => setBatchCreds(c => ({ ...c, username: e.target.value }))} placeholder="root" className="h-8 text-xs bg-slate-900 border-slate-700 text-slate-200" />
|
||||
<Label className="text-[11px] dg-text-secondary mb-1 block">用户名</Label>
|
||||
<Input value={batchCreds.username} onChange={e => setBatchCreds(c => ({ ...c, username: e.target.value }))} placeholder="root" className="h-8 text-xs dg-input text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
</div>
|
||||
{batchCreds.authType === 'password' ? (
|
||||
<div>
|
||||
<Label className="text-[11px] text-slate-400 mb-1 block">密码</Label>
|
||||
<Input type="password" value={batchCreds.password} onChange={e => setBatchCreds(c => ({ ...c, password: e.target.value }))} placeholder="输入 SSH 密码" className="h-8 text-xs bg-slate-900 border-slate-700 text-slate-200" />
|
||||
<Label className="text-[11px] dg-text-secondary mb-1 block">密码</Label>
|
||||
<Input type="password" value={batchCreds.password} onChange={e => setBatchCreds(c => ({ ...c, password: e.target.value }))} placeholder="输入 SSH 密码" className="h-8 text-xs dg-input text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Label className="text-[11px] text-slate-400 mb-1 block">SSH 私钥</Label>
|
||||
<textarea value={batchCreds.sshKey} onChange={e => setBatchCreds(c => ({ ...c, sshKey: e.target.value }))} placeholder="-----BEGIN OPENSSH PRIVATE KEY-----" className="w-full h-24 rounded-md border border-slate-700 bg-slate-900 text-slate-200 text-xs p-2 resize-none focus:outline-none focus:ring-1 focus:ring-blue-500" />
|
||||
<Label className="text-[11px] dg-text-secondary mb-1 block">SSH 私钥</Label>
|
||||
<textarea value={batchCreds.sshKey} onChange={e => setBatchCreds(c => ({ ...c, sshKey: e.target.value }))} placeholder="-----BEGIN OPENSSH PRIVATE KEY-----" className="w-full h-24 rounded-md border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] text-[var(--dg-text-primary)] text-xs p-2 resize-none focus:outline-none focus:ring-1 focus:ring-[var(--dg-blue)]" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -342,16 +351,16 @@ const AgentTab: React.FC = () => {
|
||||
{/* ⚠️ 警告文案 — 参考阿里云批量安装/升级风格 */}
|
||||
<div className="rounded-md border border-amber-500/30 bg-amber-500/5 p-3 space-y-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<Info className="w-4 h-4 text-amber-400 mt-0.5 shrink-0" />
|
||||
<Info className="w-4 h-4 dg-text-yellow mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<p className="text-xs font-medium text-amber-300 mb-1">
|
||||
{batchMode === 'install' ? '本次安装' : '本次升级'},主机监控探针将部署 AIOps Agent。AIOps Agent 是 AIOps 平台推出的新一代数据采集器,提供主机监控指标采集和可观测数据采集能力。
|
||||
</p>
|
||||
<p className="text-[11px] text-amber-400/80 leading-relaxed">
|
||||
<p className="text-[11px] dg-text-yellow/80 leading-relaxed">
|
||||
{batchMode === 'upgrade' && '升级前请注意以下事项:'}
|
||||
{batchMode === 'install' && '安装前请注意以下事项:'}
|
||||
</p>
|
||||
<ul className="mt-1 space-y-0.5 text-[11px] text-slate-400 list-disc list-inside">
|
||||
<ul className="mt-1 space-y-0.5 text-[11px] dg-text-secondary list-disc list-inside">
|
||||
{batchMode === 'upgrade' && (
|
||||
<>
|
||||
<li>如果您的机器已经安装旧版 Agent,升级操作将停止旧版并替换为最新版本。</li>
|
||||
@@ -378,16 +387,16 @@ const AgentTab: React.FC = () => {
|
||||
{/* 我已知悉 勾选 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="batch-ack" checked={batchAck} onCheckedChange={(c) => setBatchAck(!!c)} />
|
||||
<label htmlFor="batch-ack" className="text-xs text-slate-300 cursor-pointer select-none">我已知悉以上风险和注意事项</label>
|
||||
<label htmlFor="batch-ack" className="text-xs text-[var(--dg-text-secondary)] cursor-pointer select-none">我已知悉以上风险和注意事项</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300 h-8 text-xs" onClick={() => { setBatchOpen(false); setBatchAck(false); }}>
|
||||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] h-8 text-xs" onClick={() => { setBatchOpen(false); setBatchAck(false); }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white h-8 text-xs gap-1"
|
||||
className="dg-btn-primary h-8 text-xs gap-1"
|
||||
disabled={batchSubmitting || !batchAck || (batchMode === 'install' ? needInstall.length === 0 : needUpgrade.length === 0)}
|
||||
onClick={handleBatchSubmit}
|
||||
>
|
||||
@@ -423,7 +432,7 @@ const AgentTab: React.FC = () => {
|
||||
<td className="px-3 py-2.5 font-medium text-[var(--dg-text-primary)]">{h.hostname}</td>
|
||||
<td className="px-3 py-2.5 font-mono text-[var(--dg-text-secondary)]">
|
||||
{operating.has(h.server_id) ? (
|
||||
<span className="inline-flex items-center gap-1 text-blue-400">
|
||||
<span className="inline-flex items-center gap-1 dg-text-blue">
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
<span className="text-[10px]">部署中</span>
|
||||
</span>
|
||||
@@ -431,14 +440,14 @@ const AgentTab: React.FC = () => {
|
||||
h.agent_version || '—'
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2.5">{h.agent_status==='online'?<Badge className="text-[10px] bg-emerald-500/20 text-emerald-400"><Wifi className="w-3 h-3 mr-1"/>在线</Badge>:h.agent_status==='not_installed'?<Badge className="text-[10px] bg-amber-500/20 text-amber-400">未安装</Badge>:<Badge className="text-[10px] bg-slate-500/10 text-slate-400"><WifiOff className="w-3 h-3 mr-1"/>离线</Badge>}</td>
|
||||
<td className="px-3 py-2.5">{h.agent_status==='online'?<Badge className="text-[10px] bg-emerald-500/20 dg-text-green"><Wifi className="w-3 h-3 mr-1"/>在线</Badge>:h.agent_status==='not_installed'?<Badge className="text-[10px] bg-amber-500/20 dg-text-yellow">未安装</Badge>:<Badge className="text-[10px] bg-[var(--dg-bg-active)]/10 dg-text-secondary"><WifiOff className="w-3 h-3 mr-1"/>离线</Badge>}</td>
|
||||
<td className="px-3 py-2.5">{bar(Number(h.cpu_usage||0), `${Number(h.cpu_usage||0).toFixed(1)}%`)}</td>
|
||||
<td className="px-3 py-2.5">{bar(Number(h.mem_usage||0), `${Number(h.mem_usage||0).toFixed(1)}%`)}</td>
|
||||
<td className="px-3 py-2.5">{bar(Number(h.disk_usage||0), `${Number(h.disk_usage||0).toFixed(1)}%`)}</td>
|
||||
<td className="px-3 py-2.5 text-[var(--dg-text-secondary)]">{h.region||'—'}</td>
|
||||
<td className="px-3 py-2.5 font-mono text-[var(--dg-text-secondary)]">{h.ip||'—'}</td>
|
||||
<td className="px-3 py-2.5">
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs text-blue-400 hover:text-blue-300 hover:bg-blue-500/10 px-2"
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs dg-text-blue hover:bg-[var(--dg-bg-hover)] px-2"
|
||||
onClick={() => navigate(`/host-monitor?host=${encodeURIComponent(h.hostname)}`)}>
|
||||
<BarChart3 className="w-3.5 h-3.5 mr-1" />图表
|
||||
</Button>
|
||||
@@ -479,8 +488,14 @@ const ZabbixTab: React.FC = () => {
|
||||
setDs(list.find(( d: any) => d.is_default) || list[0] || null);
|
||||
}).catch(() => {}).finally(() => setLoading(false));
|
||||
}, []);
|
||||
if (loading) return <div className="text-center py-20 text-[var(--dg-text-muted)]"><Activity className="h-6 w-6 mx-auto mb-2 animate-spin" /><p className="text-xs">加载数据源…</p></div>;
|
||||
if (!ds) return <div className="text-center py-20 text-[var(--dg-text-muted)]"><Database className="h-8 w-8 mx-auto mb-2" /><p className="text-sm">未配置 Zabbix 数据源</p><a href="/datasources" className="text-xs text-blue-400 hover:underline inline-flex items-center gap-1">前往配置 <ExternalLink className="w-3 h-3" /></a></div>;
|
||||
if (loading) return <PageLoading label="加载 Zabbix 数据源..." />;
|
||||
if (!ds) return (
|
||||
<PageEmpty
|
||||
icon={<Database className="h-8 w-8" />}
|
||||
title="未配置 Zabbix 数据源"
|
||||
action={<a href="/datasources" className="text-xs dg-text-blue hover:underline inline-flex items-center gap-1">前往配置 <ExternalLink className="w-3 h-3" /></a>}
|
||||
/>
|
||||
);
|
||||
return <ZabbixDashboard dsUrl={ds.url} dsUsername={ds.username} dsPassword={ds.password} />;
|
||||
};
|
||||
|
||||
@@ -493,8 +508,14 @@ const PrometheusTab: React.FC = () => {
|
||||
setDs(list.find(( d: any) => d.is_default) || list[0] || null);
|
||||
}).catch(() => {}).finally(() => setLoading(false));
|
||||
}, []);
|
||||
if (loading) return <div className="text-center py-20 text-[var(--dg-text-muted)]"><Activity className="h-6 w-6 mx-auto mb-2 animate-spin" /><p className="text-xs">加载数据源…</p></div>;
|
||||
if (!ds) return <div className="text-center py-20 text-[var(--dg-text-muted)]"><Database className="h-8 w-8 mx-auto mb-2" /><p className="text-sm">未配置 Prometheus 数据源</p><a href="/datasources" className="text-xs text-blue-400 hover:underline inline-flex items-center gap-1">前往配置 <ExternalLink className="w-3 h-3" /></a></div>;
|
||||
if (loading) return <PageLoading label="加载 Prometheus 数据源..." />;
|
||||
if (!ds) return (
|
||||
<PageEmpty
|
||||
icon={<Database className="h-8 w-8" />}
|
||||
title="未配置 Prometheus 数据源"
|
||||
action={<a href="/datasources" className="text-xs dg-text-blue hover:underline inline-flex items-center gap-1">前往配置 <ExternalLink className="w-3 h-3" /></a>}
|
||||
/>
|
||||
);
|
||||
return <PrometheusDashboard dsUrl={ds.url} />;
|
||||
};
|
||||
|
||||
@@ -555,7 +576,7 @@ const DashboardTab: React.FC = () => {
|
||||
|
||||
{active ? (
|
||||
active.panels.length === 0 ? (
|
||||
<div className="text-center py-16 text-[var(--dg-text-muted)]"><BarChart3 className="w-10 h-10 mx-auto mb-2 opacity-50" /><p className="text-xs">暂无面板</p></div>
|
||||
<PageEmpty icon={<BarChart3 className="w-10 h-10" />} title="暂无面板" className="py-16" />
|
||||
) : (
|
||||
<div className="grid grid-cols-12 gap-3">
|
||||
{active.panels.map(p => (
|
||||
@@ -567,7 +588,7 @@ const DashboardTab: React.FC = () => {
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="text-center py-16 text-[var(--dg-text-muted)]"><Monitor className="w-10 h-10 mx-auto mb-2 opacity-50" /><p className="text-xs">暂无仪表盘</p></div>
|
||||
<PageEmpty icon={<Monitor className="w-10 h-10" />} title="暂无仪表盘" className="py-16" />
|
||||
)}
|
||||
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}><DialogContent><DialogHeader><DialogTitle>新建仪表盘</DialogTitle></DialogHeader>
|
||||
@@ -589,40 +610,30 @@ const GrafanaDashboard: React.FC = () => {
|
||||
const [tab, setTab] = useState<TabId>('agent');
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
|
||||
const pageTabs = TABS.map((item) => ({ id: item.id, label: item.label }));
|
||||
|
||||
return (
|
||||
<div className={`space-y-4 ${fullscreen ? 'fixed inset-0 z-50 bg-background p-4 overflow-auto' : ''}`}>
|
||||
|
||||
{/* Tab 切换栏 */}
|
||||
<div className="flex items-center gap-1 p-1 rounded-lg bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)]">
|
||||
{TABS.map(t => (
|
||||
<button
|
||||
key={t.id}
|
||||
onClick={() => setTab(t.id)}
|
||||
className={`flex items-center gap-1.5 px-4 py-2 rounded-md text-xs font-medium transition-colors ${
|
||||
tab === t.id
|
||||
? 'bg-[var(--dg-bg-primary)] text-[var(--dg-text-primary)] shadow-sm'
|
||||
: 'text-[var(--dg-text-muted)] hover:text-[var(--dg-text-secondary)]'
|
||||
}`}
|
||||
>
|
||||
{t.icon}
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
<div className="ml-auto">
|
||||
<Button variant="ghost" size="sm" className="h-7 w-7 p-0" onClick={() => setFullscreen(!fullscreen)}>
|
||||
{fullscreen ? <Minimize2 className="w-3.5 h-3.5" /> : <Maximize2 className="w-3.5 h-3.5" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab 内容 */}
|
||||
<div className="min-h-[50vh]">
|
||||
{tab === 'agent' && <AgentTab />}
|
||||
{tab === 'zabbix' && <ZabbixTab />}
|
||||
{tab === 'prometheus' && <PrometheusTab />}
|
||||
{tab === 'dashboard' && <DashboardTab />}
|
||||
</div>
|
||||
</div>
|
||||
<PageShell
|
||||
variant="workspace"
|
||||
title="监控大屏"
|
||||
subtitle="Agent 主机、Zabbix、Prometheus 与自定义仪表盘"
|
||||
icon={<Monitor className="h-5 w-5" />}
|
||||
tabs={pageTabs}
|
||||
activeTab={tab}
|
||||
onTabChange={(id) => setTab(id as TabId)}
|
||||
className={fullscreen ? 'fixed inset-0 z-50 bg-background p-4 overflow-auto' : undefined}
|
||||
contentClassName="min-h-[50vh]"
|
||||
actions={(
|
||||
<Button variant="ghost" size="sm" className="h-7 w-7 p-0" onClick={() => setFullscreen(!fullscreen)}>
|
||||
{fullscreen ? <Minimize2 className="w-3.5 h-3.5" /> : <Maximize2 className="w-3.5 h-3.5" />}
|
||||
</Button>
|
||||
)}
|
||||
>
|
||||
{tab === 'agent' && <AgentTab />}
|
||||
{tab === 'zabbix' && <ZabbixTab />}
|
||||
{tab === 'prometheus' && <PrometheusTab />}
|
||||
{tab === 'dashboard' && <DashboardTab />}
|
||||
</PageShell>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -147,11 +147,11 @@ export function GrafanaPanelContent({ panel, datasourceUrl }: { panel: Panel; da
|
||||
const unit = (panel.options as any)?.unit ?? '';
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full min-h-[120px]">
|
||||
<span className="text-4xl font-bold text-blue-500">
|
||||
<span className="text-4xl font-bold dg-text-blue">
|
||||
{val > 1000 ? (val / 1000).toFixed(1) + 'K' : val.toFixed(2)}
|
||||
</span>
|
||||
{unit && <span className="text-sm text-[var(--dg-text-muted)] mt-2">{unit}</span>}
|
||||
<div className="flex items-center gap-1 mt-2 text-green-500 text-sm">
|
||||
<div className="flex items-center gap-1 mt-2 dg-text-green text-sm">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
<span>实时</span>
|
||||
</div>
|
||||
@@ -205,7 +205,7 @@ export function GrafanaPanelContent({ panel, datasourceUrl }: { panel: Panel; da
|
||||
{Object.entries(row.labels).slice(0, 3).map(([k, v]) => (
|
||||
<td key={k} className="py-1 px-2 text-[var(--dg-text-secondary)]">{v}</td>
|
||||
))}
|
||||
<td className="py-1 px-2 text-right font-mono text-blue-400">{row.value.toFixed(2)}</td>
|
||||
<td className="py-1 px-2 text-right font-mono dg-text-blue">{row.value.toFixed(2)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -39,7 +39,7 @@ function getUsageBar(val: number) {
|
||||
const color = v > 85 ? 'bg-red-500' : v > 60 ? 'bg-amber-500' : 'bg-emerald-500';
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 min-w-[80px]">
|
||||
<div className="flex-1 h-1.5 rounded-full bg-slate-700/50 overflow-hidden">
|
||||
<div className="flex-1 h-1.5 rounded-full bg-[var(--dg-bg-active)]/50 overflow-hidden">
|
||||
<div className={`h-full rounded-full ${color}`} style={{ width: `${Math.min(v, 100)}%` }} />
|
||||
</div>
|
||||
<span className="text-xs font-mono w-10 text-right text-[var(--dg-text-primary)]">{v.toFixed(0)}%</span>
|
||||
@@ -126,10 +126,10 @@ export default function HostMonitorDashboard() {
|
||||
};
|
||||
|
||||
const SortIcon = ({ field }: { field: SortField }) => {
|
||||
if (sortField !== field) return <ArrowUpDown className="w-3 h-3 text-slate-500" />;
|
||||
if (sortField !== field) return <ArrowUpDown className="w-3 h-3 dg-text-muted" />;
|
||||
return sortDir === 'asc'
|
||||
? <ArrowUp className="w-3 h-3 text-blue-400" />
|
||||
: <ArrowDown className="w-3 h-3 text-blue-400" />;
|
||||
? <ArrowUp className="w-3 h-3 dg-text-blue" />
|
||||
: <ArrowDown className="w-3 h-3 dg-text-blue" />;
|
||||
};
|
||||
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
@@ -141,10 +141,10 @@ export default function HostMonitorDashboard() {
|
||||
|
||||
// 状态标签颜色
|
||||
const statusBadge = (status: string) => {
|
||||
if (status === 'online') return <Badge className="bg-emerald-500/20 text-emerald-400 border-emerald-500/30 text-[11px]">在线</Badge>;
|
||||
if (status === 'running') return <Badge className="bg-blue-500/20 text-blue-400 border-blue-500/30 text-[11px]">运行中</Badge>;
|
||||
if (status === 'stopped') return <Badge className="bg-slate-500/20 text-slate-400 border-slate-500/30 text-[11px]">已停止</Badge>;
|
||||
return <Badge variant="outline" className="text-[11px] text-slate-500">{status || '--'}</Badge>;
|
||||
if (status === 'online') return <Badge className="bg-emerald-500/20 dg-text-green border-emerald-500/30 text-[11px]">在线</Badge>;
|
||||
if (status === 'running') return <Badge className="bg-blue-500/20 dg-text-blue border-blue-500/30 text-[11px]">运行中</Badge>;
|
||||
if (status === 'stopped') return <Badge className="bg-[var(--dg-bg-active)]/20 dg-text-secondary border-[var(--dg-border)] text-[11px]">已停止</Badge>;
|
||||
return <Badge variant="outline" className="text-[11px] dg-text-muted">{status || '--'}</Badge>;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -153,7 +153,7 @@ export default function HostMonitorDashboard() {
|
||||
<div className="grid grid-cols-6 gap-3">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-3 flex items-center gap-2.5">
|
||||
<div className="p-1.5 rounded-lg bg-blue-500/10"><Server className="h-4 w-4 text-blue-400" /></div>
|
||||
<div className="p-1.5 rounded-lg bg-blue-500/10"><Server className="h-4 w-4 dg-text-blue" /></div>
|
||||
<div>
|
||||
<div className="text-xl font-bold text-[var(--dg-text-primary)]">{stats.total}</div>
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)]">主机总数</div>
|
||||
@@ -162,18 +162,18 @@ export default function HostMonitorDashboard() {
|
||||
</Card>
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-3 flex items-center gap-2.5">
|
||||
<div className="p-1.5 rounded-lg bg-emerald-500/10"><Wifi className="h-4 w-4 text-emerald-400" /></div>
|
||||
<div className="p-1.5 rounded-lg bg-emerald-500/10"><Wifi className="h-4 w-4 dg-text-green" /></div>
|
||||
<div>
|
||||
<div className="text-xl font-bold text-emerald-400">{stats.online}</div>
|
||||
<div className="text-xl font-bold dg-text-green">{stats.online}</div>
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)]">Agent 在线</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-3 flex items-center gap-2.5">
|
||||
<div className="p-1.5 rounded-lg bg-slate-500/10"><WifiOff className="h-4 w-4 text-slate-400" /></div>
|
||||
<div className="p-1.5 rounded-lg bg-[var(--dg-bg-active)]/10"><WifiOff className="h-4 w-4 dg-text-secondary" /></div>
|
||||
<div>
|
||||
<div className="text-xl font-bold text-slate-400">{stats.offline}</div>
|
||||
<div className="text-xl font-bold dg-text-secondary">{stats.offline}</div>
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)]">离线</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -182,12 +182,12 @@ export default function HostMonitorDashboard() {
|
||||
const onlines = hosts.filter((h: JsonData) => h.agent_status === 'online');
|
||||
const avgCpu = onlines.length > 0 ? onlines.reduce((s: number, h: JsonData) => s + (h.cpu_usage || 0), 0) / onlines.length : 0;
|
||||
const avgMem = onlines.length > 0 ? onlines.reduce((s: number, h: JsonData) => s + (h.memory_usage || 0), 0) / onlines.length : 0;
|
||||
const cpuColor = avgCpu > 85 ? 'text-red-400' : avgCpu > 60 ? 'text-amber-400' : 'text-emerald-400';
|
||||
const memColor = avgMem > 85 ? 'text-red-400' : avgMem > 60 ? 'text-amber-400' : 'text-emerald-400';
|
||||
const cpuColor = avgCpu > 85 ? 'dg-text-red' : avgCpu > 60 ? 'dg-text-yellow' : 'dg-text-green';
|
||||
const memColor = avgMem > 85 ? 'dg-text-red' : avgMem > 60 ? 'dg-text-yellow' : 'dg-text-green';
|
||||
return (<>
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-3 flex items-center gap-2.5">
|
||||
<div className="p-1.5 rounded-lg bg-red-500/10"><Cpu className="h-4 w-4 text-red-400" /></div>
|
||||
<div className="p-1.5 rounded-lg bg-red-500/10"><Cpu className="h-4 w-4 dg-text-red" /></div>
|
||||
<div>
|
||||
<div className={`text-xl font-bold ${cpuColor}`}>{avgCpu.toFixed(1)}<span className="text-sm">%</span></div>
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)]">平均 CPU</div>
|
||||
@@ -196,7 +196,7 @@ export default function HostMonitorDashboard() {
|
||||
</Card>
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-3 flex items-center gap-2.5">
|
||||
<div className="p-1.5 rounded-lg bg-amber-500/10"><Server className="h-4 w-4 text-amber-400" /></div>
|
||||
<div className="p-1.5 rounded-lg bg-amber-500/10"><Server className="h-4 w-4 dg-text-yellow" /></div>
|
||||
<div>
|
||||
<div className={`text-xl font-bold ${memColor}`}>{avgMem.toFixed(1)}<span className="text-sm">%</span></div>
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)]">平均内存</div>
|
||||
@@ -214,9 +214,9 @@ export default function HostMonitorDashboard() {
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm flex items-center gap-2 text-[var(--dg-text-primary)]">
|
||||
<Cpu className="h-4 w-4 text-red-400" />
|
||||
<Cpu className="h-4 w-4 dg-text-red" />
|
||||
CPU 使用率 Top 5
|
||||
<Badge variant="outline" className="text-[10px] bg-red-500/10 text-red-400 border-red-500/30">阈值: 60% / 85%</Badge>
|
||||
<Badge variant="outline" className="text-[10px] bg-red-500/10 dg-text-red border-red-500/30">阈值: 60% / 85%</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
@@ -229,7 +229,7 @@ export default function HostMonitorDashboard() {
|
||||
return (
|
||||
<div key={h.id} className="flex items-center gap-2">
|
||||
<span className="text-xs text-[var(--dg-text-secondary)] w-20 truncate" title={h.hostname}>{h.hostname}</span>
|
||||
<div className="flex-1 h-4 rounded bg-slate-700/50 overflow-hidden">
|
||||
<div className="flex-1 h-4 rounded bg-[var(--dg-bg-active)]/50 overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded transition-all"
|
||||
style={{
|
||||
@@ -243,7 +243,7 @@ export default function HostMonitorDashboard() {
|
||||
/>
|
||||
</div>
|
||||
<span className={`text-xs font-mono w-10 text-right ${
|
||||
val >= 85 ? 'text-red-400' : val >= 60 ? 'text-amber-400' : 'text-emerald-400'
|
||||
val >= 85 ? 'dg-text-red' : val >= 60 ? 'dg-text-yellow' : 'dg-text-green'
|
||||
}`}>{val.toFixed(1)}%</span>
|
||||
</div>
|
||||
);
|
||||
@@ -255,9 +255,9 @@ export default function HostMonitorDashboard() {
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm flex items-center gap-2 text-[var(--dg-text-primary)]">
|
||||
<Server className="h-4 w-4 text-amber-400" />
|
||||
<Server className="h-4 w-4 dg-text-yellow" />
|
||||
内存使用率 Top 5
|
||||
<Badge variant="outline" className="text-[10px] bg-amber-500/10 text-amber-400 border-amber-500/30">阈值: 60% / 85%</Badge>
|
||||
<Badge variant="outline" className="text-[10px] bg-amber-500/10 dg-text-yellow border-amber-500/30">阈值: 60% / 85%</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
@@ -270,7 +270,7 @@ export default function HostMonitorDashboard() {
|
||||
return (
|
||||
<div key={h.id} className="flex items-center gap-2">
|
||||
<span className="text-xs text-[var(--dg-text-secondary)] w-20 truncate" title={h.hostname}>{h.hostname}</span>
|
||||
<div className="flex-1 h-4 rounded bg-slate-700/50 overflow-hidden">
|
||||
<div className="flex-1 h-4 rounded bg-[var(--dg-bg-active)]/50 overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded transition-all"
|
||||
style={{
|
||||
@@ -284,7 +284,7 @@ export default function HostMonitorDashboard() {
|
||||
/>
|
||||
</div>
|
||||
<span className={`text-xs font-mono w-10 text-right ${
|
||||
val >= 85 ? 'text-red-400' : val >= 60 ? 'text-amber-400' : 'text-emerald-400'
|
||||
val >= 85 ? 'dg-text-red' : val >= 60 ? 'dg-text-yellow' : 'dg-text-green'
|
||||
}`}>{val.toFixed(1)}%</span>
|
||||
</div>
|
||||
);
|
||||
@@ -299,13 +299,13 @@ export default function HostMonitorDashboard() {
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="h-5 w-5 text-emerald-400" />
|
||||
<Activity className="h-5 w-5 dg-text-green" />
|
||||
<CardTitle className="text-base text-[var(--dg-text-primary)]">主机监控列表</CardTitle>
|
||||
<Badge variant="outline" className="text-xs bg-emerald-500/10 text-emerald-400 border-emerald-500/30">实时</Badge>
|
||||
<Badge variant="outline" className="text-xs bg-emerald-500/10 dg-text-green border-emerald-500/30">实时</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
|
||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 dg-text-muted" />
|
||||
<Input
|
||||
className="w-[200px] pl-8 h-8 text-sm bg-[var(--dg-bg-primary)]"
|
||||
placeholder="搜索主机名/IP..."
|
||||
@@ -324,12 +324,12 @@ export default function HostMonitorDashboard() {
|
||||
<CardContent className="p-0">
|
||||
{loading ? (
|
||||
<div className="py-16 text-center text-[var(--dg-text-muted)]">
|
||||
<Activity className="h-8 w-8 mx-auto mb-2 animate-spin text-emerald-400" />
|
||||
<Activity className="h-8 w-8 mx-auto mb-2 animate-spin dg-text-green" />
|
||||
<p className="text-sm">加载中...</p>
|
||||
</div>
|
||||
) : sorted.length === 0 ? (
|
||||
<div className="py-16 text-center text-[var(--dg-text-muted)]">
|
||||
<Server className="h-10 w-10 mx-auto mb-2 text-slate-600" />
|
||||
<Server className="h-10 w-10 mx-auto mb-2 dg-text-muted" />
|
||||
<p className="text-sm">暂无主机数据</p>
|
||||
<p className="text-xs mt-1">请先添加服务器,或等待 Agent 上报数据</p>
|
||||
</div>
|
||||
@@ -369,7 +369,7 @@ export default function HostMonitorDashboard() {
|
||||
{/* 主机名/ID */}
|
||||
<td className="px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full flex-shrink-0 ${h.agent_status === 'online' ? 'bg-emerald-400' : h.status === 'running' ? 'bg-blue-400' : 'bg-slate-500'}`} />
|
||||
<div className={`w-2 h-2 rounded-full flex-shrink-0 ${h.agent_status === 'online' ? 'bg-emerald-400' : h.status === 'running' ? 'bg-blue-400' : 'bg-[var(--dg-bg-active)]'}`} />
|
||||
<div>
|
||||
<div className="text-sm font-medium text-[var(--dg-text-primary)]">{h.hostname}</div>
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)]">ID: {h.id}</div>
|
||||
@@ -387,21 +387,21 @@ export default function HostMonitorDashboard() {
|
||||
{/* CPU 使用率 */}
|
||||
<td className="px-3 py-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Cpu className="h-3 w-3 text-red-400 flex-shrink-0" />
|
||||
<Cpu className="h-3 w-3 dg-text-red flex-shrink-0" />
|
||||
{getUsageBar(h.cpu_usage)}
|
||||
</div>
|
||||
</td>
|
||||
{/* 内存使用率 */}
|
||||
<td className="px-3 py-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Server className="h-3 w-3 text-amber-400 flex-shrink-0" />
|
||||
<Server className="h-3 w-3 dg-text-yellow flex-shrink-0" />
|
||||
{getUsageBar(h.memory_usage)}
|
||||
</div>
|
||||
</td>
|
||||
{/* 磁盘使用率 */}
|
||||
<td className="px-3 py-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<HardDrive className="h-3 w-3 text-orange-400 flex-shrink-0" />
|
||||
<HardDrive className="h-3 w-3 dg-text-orange flex-shrink-0" />
|
||||
{getUsageBar(h.disk_usage)}
|
||||
</div>
|
||||
</td>
|
||||
@@ -413,7 +413,7 @@ export default function HostMonitorDashboard() {
|
||||
</td>
|
||||
{/* IP 地址 */}
|
||||
<td className="px-3 py-2">
|
||||
<code className="text-xs text-slate-300 font-mono">{h.ip || '--'}</code>
|
||||
<code className="text-xs text-[var(--dg-text-secondary)] font-mono">{h.ip || '--'}</code>
|
||||
</td>
|
||||
{/* 操作栏 */}
|
||||
<td className="px-3 py-2">
|
||||
@@ -421,7 +421,7 @@ export default function HostMonitorDashboard() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs text-blue-400 hover:text-blue-300 hover:bg-blue-500/10"
|
||||
className="h-7 text-xs dg-text-blue hover:text-blue-300 hover:bg-blue-500/10"
|
||||
onClick={() => navigate(`/monitor-center?host=${encodeURIComponent(h.hostname)}`)}
|
||||
title="查看监控图表"
|
||||
>
|
||||
@@ -431,7 +431,7 @@ export default function HostMonitorDashboard() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs text-amber-400 hover:text-amber-300 hover:bg-amber-500/10"
|
||||
className="h-7 text-xs dg-text-yellow hover:text-amber-300 hover:bg-amber-500/10"
|
||||
onClick={() => navigate(`/alerts?hostname=${encodeURIComponent(h.hostname)}`)}
|
||||
title="查看报警规则"
|
||||
>
|
||||
|
||||
@@ -58,7 +58,7 @@ const K8sPanel: React.FC<{ d: JsonData}> = ({ d }) => (
|
||||
]}
|
||||
/>
|
||||
<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>
|
||||
<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>;
|
||||
@@ -82,7 +82,7 @@ const NodePanel: React.FC<{ d: JsonData}> = ({ d }) => (
|
||||
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 text-emerald-400':'bg-red-500/20 text-red-400'}`}>{up?.value===1?'UP':'DOWN'}</Badge></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>
|
||||
@@ -116,8 +116,8 @@ const JvmPanel: React.FC<{ d: JsonData}> = ({ d }) => {
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@@ -168,15 +168,15 @@ export const PrometheusDashboard: React.FC<Props> = ({ dsUrl }) => {
|
||||
<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" />
|
||||
<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 text-purple-400">{targets} targets</Badge>}
|
||||
{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 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>)
|
||||
{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">
|
||||
|
||||
@@ -66,8 +66,8 @@ export const ZabbixDashboard: React.FC<Props> = ({ dsUrl, dsUsername, dsPassword
|
||||
});
|
||||
|
||||
const SI = ({ f }: { f: SortField }) => {
|
||||
if (sortField !== f) 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" />;
|
||||
if (sortField !== f) return <ArrowUpDown className="w-3 h-3 dg-text-muted" />;
|
||||
return sortDir === 'asc' ? <ArrowUp className="w-3 h-3 dg-text-blue" /> : <ArrowDown className="w-3 h-3 dg-text-blue" />;
|
||||
};
|
||||
|
||||
const bar = (v: number, label: string) => (
|
||||
@@ -75,7 +75,7 @@ export const ZabbixDashboard: React.FC<Props> = ({ dsUrl, dsUsername, dsPassword
|
||||
<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 ${v >= 80 ? 'bg-orange-500' : 'bg-blue-500'}`} style={{ width: `${Math.min(v, 100)}%` }} />
|
||||
</div>
|
||||
<span className={`w-14 text-right text-xs font-mono tabular-nums ${v >= 80 ? 'text-orange-400' : ''}`}>{label}</span>
|
||||
<span className={`w-14 text-right text-xs font-mono tabular-nums ${v >= 80 ? 'dg-text-orange' : ''}`}>{label}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -93,17 +93,17 @@ export const ZabbixDashboard: React.FC<Props> = ({ dsUrl, dsUsername, dsPassword
|
||||
<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" />
|
||||
<Activity className="h-4 w-4 dg-text-blue" />
|
||||
<CardTitle className="text-sm font-medium">Zabbix 主机监控</CardTitle>
|
||||
{version && <Badge className="text-[10px] bg-blue-500/10 text-blue-400">v{version}</Badge>}
|
||||
{version && <Badge className="text-[10px] bg-blue-500/10 dg-text-blue">v{version}</Badge>}
|
||||
{connected && <span className="text-[10px] text-[var(--dg-text-muted)] ml-2">{hosts.length} 台</span>}
|
||||
{connected && <div className="ml-auto flex gap-1"><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-blue-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 flex items-start gap-2"><AlertTriangle className="h-4 w-4 mt-0.5" /><span>{error}</span></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>)
|
||||
{loading ? (<div className="text-center py-12"><Loader2 className="h-6 w-6 mx-auto mb-2 animate-spin dg-text-blue" /><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 flex items-start gap-2"><AlertTriangle className="h-4 w-4 mt-0.5" /><span>{error}</span></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 dg-text-muted" /><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">
|
||||
@@ -119,14 +119,14 @@ export const ZabbixDashboard: React.FC<Props> = ({ dsUrl, dsUsername, dsPassword
|
||||
<tbody>
|
||||
{sorted.map((h, i) => (
|
||||
<tr key={h.hostid || i} className="border-b border-[var(--dg-border)] last:border-0 hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||
<td className="px-3 py-2.5 whitespace-nowrap"><Badge className={`text-[10px] ${h.available === '1' ? 'bg-emerald-500/20 text-emerald-400' : 'bg-red-500/20 text-red-400'}`}><Wifi className="w-3 h-3 mr-1" />{h.available === '1' ? 'UP' : 'DOWN'}</Badge> <span className="font-medium text-[var(--dg-text-primary)] ml-1">{h.name || h.hostname}</span></td>
|
||||
<td className="px-3 py-2.5 whitespace-nowrap"><Badge className={`text-[10px] ${h.available === '1' ? 'bg-emerald-500/20 dg-text-green' : 'bg-red-500/20 dg-text-red'}`}><Wifi className="w-3 h-3 mr-1" />{h.available === '1' ? 'UP' : 'DOWN'}</Badge> <span className="font-medium text-[var(--dg-text-primary)] ml-1">{h.name || h.hostname}</span></td>
|
||||
<td className="px-3 py-2.5">{bar(Number(h.cpu_usage || 0), `${Number(h.cpu_usage || 0).toFixed(1)}%`)}</td>
|
||||
<td className="px-3 py-2.5">{bar(Number(h.memory_usage || 0), `${Number(h.memory_usage || 0).toFixed(1)}%`)}</td>
|
||||
<td className="px-3 py-2.5">{bar(Number(h.disk_usage || 0), `${Number(h.disk_usage || 0).toFixed(1)}%`)}</td>
|
||||
<td className="px-3 py-2.5 text-[var(--dg-text-secondary)]">{h.load5 > 0 ? h.load5.toFixed(1) : '—'}</td>
|
||||
<td className="px-3 py-2.5 font-mono text-[var(--dg-text-secondary)]">{fmtBps(h.net_in_bps)}</td>
|
||||
<td className="px-3 py-2.5 font-mono text-[var(--dg-text-secondary)]">{fmtBps(h.net_out_bps)}</td>
|
||||
<td className="px-3 py-2.5 text-center"><span className={`font-mono font-bold ${h.problem_count > 0 ? (h.problem_count > 2 ? 'text-red-400' : 'text-amber-400') : 'text-emerald-400'}`}>{h.problem_count > 0 ? <><AlertTriangle className="w-3 h-3 inline mr-1" />{h.problem_count}</> : '0'}</span></td>
|
||||
<td className="px-3 py-2.5 text-center"><span className={`font-mono font-bold ${h.problem_count > 0 ? (h.problem_count > 2 ? 'dg-text-red' : 'dg-text-yellow') : 'dg-text-green'}`}>{h.problem_count > 0 ? <><AlertTriangle className="w-3 h-3 inline mr-1" />{h.problem_count}</> : '0'}</span></td>
|
||||
<td className="px-3 py-2.5 text-[var(--dg-text-secondary)]">{h.uptime_days > 0 ? `${h.uptime_days}d` : '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
Reference in new issue
Block a user