// 监控大屏 — 标签页切换 Agent / Zabbix / Prometheus + 自定义仪表盘 // Logan.Li import React, { useState, useEffect, useMemo } from 'react'; import { Monitor, Plus, RefreshCw, Maximize2, Minimize2, BarChart3, Clock, Server, Activity, Database, Wifi, WifiOff, ArrowUpDown, ArrowUp, ArrowDown, ExternalLink, Download, Rocket, PackageOpen, Loader2, Info, Trash2 } 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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Label } from '@/components/ui/label'; import { toast } from 'sonner'; import { useUserStore } from '@/store'; import { dashboardApi, promDatasourceApi, zabbixDatasourceApi, agentApi, serverApi } from '@/services/api'; import { GrafanaPanelContent } from './GrafanaPanelContent'; import { ZabbixDashboard } from './ZabbixDashboard'; import { PrometheusDashboard } from './PrometheusDashboard'; import { http } from '@/services/http'; import { asList } from '@/components/agent/agentUtils'; import { AgentWizard } from '@/components/agent/AgentWizard'; import { Checkbox } from '@/components/ui/checkbox'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { useNavigate } from 'react-router-dom'; // ═══════════ Tab 配置 ═══════════ type TabId = 'agent' | 'zabbix' | 'prometheus' | 'dashboard'; const TABS: { id: TabId; label: string; icon: React.ReactNode }[] = [ { id: 'agent', label: 'Agent 主机', icon: }, { id: 'zabbix', label: 'Zabbix', icon: }, { id: 'prometheus', label: 'Prometheus', icon: }, { id: 'dashboard', label: '自定义', icon: }, ]; // ═══════════ Agent 主机监控表格 ═══════════ interface AgentHost { server_id: number; hostname: string; ip: string; cpu_usage: number; cpu_cores: number; mem_usage: number; mem_total: number; mem_used: number; disk_usage: number; agent_version: string; agent_status: string; os: string; region: string; } type SortField = 'hostname' | 'agent_version' | 'agent_status' | 'cpu_usage' | 'mem_usage' | 'disk_usage' | 'region' | 'ip'; const AgentTab: React.FC = () => { const _confirm = useConfirm(); const navigate = useNavigate(); const [hosts, setHosts] = useState([]); const [loading, setLoading] = useState(true); const [sf, setSf] = useState('cpu_usage'); const [sd, setSd] = useState<'asc' | 'desc'>('desc'); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); const [selected, setSelected] = useState>(new Set()); const [wizardOpen, setWizardOpen] = useState(false); const [wizardServer, setWizardServer] = useState(null); // 批量部署对话框 const [batchOpen, setBatchOpen] = useState(false); const [batchMode, setBatchMode] = useState<'install' | 'upgrade'>('install'); const [batchAck, setBatchAck] = useState(false); const [batchSubmitting, setBatchSubmitting] = useState(false); // 正在执行操作的主机 ID 集合(版本列 loading) const [operating, setOperating] = useState>(new Set()); // 批量卸载 const [batchUninstalling, setBatchUninstalling] = useState(false); const handleBatchUninstall = async () => { const ok = await _confirm({ title: '批量卸载 Agent', description: `确定卸载选中的 ${selIds.length} 台主机的 AIOps Agent?卸载将在后台异步执行。`, confirmText: '卸载', variant: 'destructive' }); if (!ok) return; setBatchUninstalling(true); try { await agentApi.batchUninstall(selIds); toast.success(`已提交:批量卸载 ${selIds.length} 台主机的 Agent,后台执行中,30秒后自动刷新`); setSelected(new Set()); setTimeout(() => fetch(), 30000); } catch { toast.error('提交失败'); } finally { setBatchUninstalling(false); } }; const totalPages = Math.max(1, Math.ceil(hosts.length / pageSize)); const fetch = async () => { try { setHosts(asList(await http.get('/agent/latest'))); } catch {} finally { setLoading(false); } }; useEffect(() => { fetch(); const t = setInterval(fetch, 30000); return () => clearInterval(t); }, []); const toggle = (f: SortField) => { if (sf === f) setSd(d => d === 'asc' ? 'desc' : 'asc'); else { setSf(f); setSd('desc'); } }; const sorted = [...hosts].sort((a, b) => { let va: any = (a as any)[sf] ?? '', vb: any = (b as any)[sf] ?? ''; if (sf === 'agent_status') { va = a.agent_status || ''; vb = b.agent_status || ''; } if (typeof va === 'number') return sd === 'asc' ? va - vb : vb - va; 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 ? : sd === 'asc' ? : ; const bar = (v: number, l: string) => (
= 80 ? 'bg-orange-500' : 'bg-emerald-500'}`} style={{ width: `${Math.min(v, 100)}%` }} />
= 80 ? 'text-orange-400' : ''}`}>{l}
); const selAll = selected.size === hosts.length && hosts.length > 0; const selAny = selected.size > 0; const selIds = [...selected].map(Number).filter(n => !isNaN(n)); // 选中主机按安装/在线分组 const selectedHosts = useMemo(() => { return hosts.filter(h => selected.has(String(h.server_id))); }, [hosts, selected]); const needInstall = useMemo(() => selectedHosts.filter(h => h.agent_status !== 'online'), [selectedHosts]); const needUpgrade = useMemo(() => selectedHosts.filter(h => h.agent_status === 'online'), [selectedHosts]); // 打开批量部署对话框 const openBatchDialog = () => { setBatchAck(false); // 自动推断模式: 全无Agent → install,全在线 → upgrade,混合 → install if (needInstall.length > 0 && needUpgrade.length === 0) setBatchMode('install'); else if (needInstall.length === 0 && needUpgrade.length > 0) setBatchMode('upgrade'); else setBatchMode('install'); setBatchOpen(true); }; // 执行批量部署 const handleBatchSubmit = async () => { if (!batchAck) { toast.error('请先确认并勾选「我已知悉」'); return; } setBatchSubmitting(true); const token = useUserStore.getState().token; const ids = selectedHosts.map(h => h.server_id); setOperating(new Set(ids)); try { if (batchMode === 'install') { if (!token) { toast.error('未登录'); return; } // 安装:只安装无 Agent 的 const installIds = needInstall.map(h => h.server_id); if (installIds.length === 0) { toast.warning('所选主机均已安装 Agent,无需安装'); return; } const res = await serverApi.batchInstallAgent(installIds, token); toast.success(`已提交:批量安装 ${res?.total ?? installIds.length} 台主机的 Agent,后台执行中,30秒后自动刷新`); setSelected(new Set()); setTimeout(() => { fetch(); setOperating(new Set()); }, 30000); } else { // 升级:只升级在线的 const upgradeIds = needUpgrade.map(h => h.server_id); if (upgradeIds.length === 0) { toast.warning('所选主机中没有在线 Agent,无法升级'); return; } const res = await agentApi.batchUpgrade(upgradeIds); toast.success(res?.message || `已提交:批量升级 ${upgradeIds.length} 台主机的 Agent,后台执行中,30秒后自动刷新`); setSelected(new Set()); setTimeout(() => { fetch(); setOperating(new Set()); }, 30000); } setBatchOpen(false); } catch { toast.error('提交失败'); setOperating(new Set()); } finally { setBatchSubmitting(false); } }; if (loading) return

加载中…

; if (!hosts.length) return

暂无服务器资产

请先在「服务器管理」中添加主机,然后在此安装 Agent

; return (
{/* 工具栏 */}
{selAny && 已选 {selected.size} 项}
{selAny && ( <> )}
{ toast.success('Agent 安装成功'); setWizardOpen(false); fetch(); }} onFailed={() => toast.error('安装失败')} /> {/* ────────── 批量部署 Agent 对话框 ────────── */} { if (!v) { setBatchOpen(false); setBatchAck(false); } }}> 批量部署 Agent 选中 {selectedHosts.length} 台主机(需安装: {needInstall.length},待升级: {needUpgrade.length})
{/* 模式选择 — 类似阿里云安装/升级切换 */}
{(['install', 'upgrade'] as const).map(m => ( ))}
{/* 主机列表 */}
{(batchMode === 'install' ? needInstall : needUpgrade).map(h => ( ))}
主机名 IP 状态 当前版本 操作
{h.hostname} {h.ip} {h.agent_status === 'online' ? 在线 : {h.agent_status === 'not_installed' ? '未安装' : h.agent_status || '离线'} } {h.agent_version || '—'} {batchMode === 'install' ? '即将安装' : '即将升级'}
{/* ⚠️ 警告文案 — 参考阿里云批量安装/升级风格 */}

{batchMode === 'install' ? '本次安装' : '本次升级'},主机监控探针将部署 AIOps Agent。AIOps Agent 是 AIOps 平台推出的新一代数据采集器,提供主机监控指标采集和可观测数据采集能力。

{batchMode === 'upgrade' && '升级前请注意以下事项:'} {batchMode === 'install' && '安装前请注意以下事项:'}

    {batchMode === 'upgrade' && ( <>
  • 如果您的机器已经安装旧版 Agent,升级操作将停止旧版并替换为最新版本。
  • 如果您的机器之前安装过 Agent 但已卸载,遗留有相关采集配置,本次升级可能导致配置重新生效。升级前请确保存量废弃配置已经移除。
  • 如果您的机器采用自定义安装的 Agent(非 AIOps 提供的一键安装脚本),请不要直接使用批量升级。
  • 批量升级前,请务必先单机灰度升级,确保升级成功且不影响业务后再操作。
  • )} {batchMode === 'install' && ( <>
  • 安装将自动检测目标主机架构(amd64/arm64),下载对应版本的 Agent 二进制并启动服务。
  • 请确保目标主机的 SSH 端口(默认 22)可访问,且已关联有效的 SSH 凭据。
  • 安装过程约 30-60 秒,期间目标主机 CPU 负载会短暂升高。
  • 若安装失败,请检查目标主机的防火墙规则和 SSH 服务状态后重试。
  • )}
  • 操作不可回滚,请在操作前确认已备份关键配置。
  • 操作将在后台异步执行,您可以在「任务执行记录」中查看详细进度。
{/* 我已知悉 勾选 */}
setBatchAck(!!c)} />
{['hostname','agent_version','agent_status','cpu_usage','mem_usage','disk_usage','region','ip'].map(f => ( ))} {paged.map((h, i) => ( ))}
setSelected(c ? new Set(hosts.map(h => String(h.server_id))) : new Set())} /> toggle(f as SortField)}>
{{hostname:'主机名',agent_version:'Agent版本',agent_status:'状态',cpu_usage:'CPU',mem_usage:'内存',disk_usage:'磁盘',region:'区域',ip:'IP'}[f]}
操作
{ const next = new Set(selected); c ? next.add(String(h.server_id)) : next.delete(String(h.server_id)); setSelected(next); }} /> {h.hostname} {operating.has(h.server_id) ? ( 部署中 ) : ( h.agent_version || '—' )} {h.agent_status==='online'?在线:h.agent_status==='not_installed'?未安装:离线} {bar(Number(h.cpu_usage||0), `${Number(h.cpu_usage||0).toFixed(1)}%`)} {bar(Number(h.mem_usage||0), `${Number(h.mem_usage||0).toFixed(1)}%`)} {bar(Number(h.disk_usage||0), `${Number(h.disk_usage||0).toFixed(1)}%`)} {h.region||'—'} {h.ip||'—'}
{hosts.length > pageSize && (
共 {hosts.length} 条
{page} / {totalPages}
)}
); }; // ═══════════ 数据源 Wrapper ═══════════ const ZabbixTab: React.FC = () => { const [ds, setDs] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { zabbixDatasourceApi.list().then((r: any) => { const list = r?.data || r || []; setDs(list.find((d: any) => d.is_default) || list[0] || null); }).catch(() => {}).finally(() => setLoading(false)); }, []); if (loading) return

加载数据源…

; if (!ds) return

未配置 Zabbix 数据源

前往配置
; return ; }; const PrometheusTab: React.FC = () => { const [ds, setDs] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { promDatasourceApi.list().then((r: any) => { const list = r?.data || r || []; setDs(list.find((d: any) => d.is_default) || list[0] || null); }).catch(() => {}).finally(() => setLoading(false)); }, []); if (loading) return

加载数据源…

; if (!ds) return

未配置 Prometheus 数据源

前往配置
; return ; }; // ═══════════ 自定义仪表盘 ═══════════ interface Dashboard { id: string; name: string; description: string; datasource: string; datasourceUrl: string; panels: Panel[]; createdAt: string; updatedAt: string; createdBy: string; isDefault: boolean; } export interface Panel { id: string; title: string; type: string; query: string; datasource: string; width: number; height: number; x: number; y: number; options: any; } const DashboardTab: React.FC = () => { const [dashboards, setDashboards] = useState([]); const [active, setActive] = useState(null); const [createOpen, setCreateOpen] = useState(false); const [addOpen, setAddOpen] = useState(false); const [nd, setNd] = useState({ name: '', description: '', datasource: 'prometheus' as const, datasourceUrl: '' }); const [np, setNp] = useState({ title: '', type: 'graph' as const, query: '', width: 6, height: 4 }); const fetch = async () => { try { const res = await dashboardApi.getDashboards(); const list = res.data || []; setDashboards(list); if (list.length > 0 && !active) setActive(list[0]); } catch {} }; useEffect(() => { fetch(); }, []); const create = async () => { if (!nd.name) { toast.error('请输入名称'); return; } try { await dashboardApi.createDashboard({ ...nd, panels: [] }); toast.success('创建成功'); setCreateOpen(false); setNd({ name: '', description: '', datasource: 'prometheus', datasourceUrl: '' }); fetch(); } catch { toast.error('创建失败'); } }; const addPanel = async () => { if (!np.title || !np.query || !active) return; try { const p: Panel = { id: Date.now().toString(), ...np, datasource: active.datasource, x: 0, y: 0, options: { legend: true, tooltip: true, refreshInterval: 30 } }; await dashboardApi.updateDashboard(active.id, { ...active, panels: [...active.panels, p] }); toast.success('面板添加成功'); setAddOpen(false); setNp({ title: '', type: 'graph', query: '', width: 6, height: 4 }); fetch(); } catch { toast.error('添加失败'); } }; return (
{active && }
{active ? ( active.panels.length === 0 ? (

暂无面板

) : (
{active.panels.map(p => ( {p.title} ))}
) ) : (

暂无仪表盘

)} 新建仪表盘
setNd({ ...nd, name: e.target.value })} />
添加面板
setNp({ ...np, title: e.target.value })} />
setNp({ ...np, query: e.target.value })} />
); }; // ═══════════ 主组件 ═══════════ const GrafanaDashboard: React.FC = () => { const [tab, setTab] = useState('agent'); const [fullscreen, setFullscreen] = useState(false); return (
{/* Tab 切换栏 */}
{TABS.map(t => ( ))}
{/* Tab 内容 */}
{tab === 'agent' && } {tab === 'zabbix' && } {tab === 'prometheus' && } {tab === 'dashboard' && }
); }; export default GrafanaDashboard;