import { useState, useEffect, useRef, useCallback } from 'react'; import type { ComponentType } from 'react'; import { toast } from 'sonner'; import { agentApi } from '@/api'; import { AgentWizard } from './AgentWizard'; import { HostDetailDialog } from './HostDetailDialog'; import { isOnline, timeAgo, asList } from './agentUtils'; import type { AgentHostRecord } from '@/types'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Badge } from '@/components/ui/badge'; import { Progress } from '@/components/ui/progress'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Cpu, MemoryStick, HardDrive, Wifi, WifiOff, RefreshCw, Terminal, Rocket, Activity, Network, Loader2, Clock, Server, } from 'lucide-react'; // ============ 组件 ============ export function AgentMonitor() { const [hosts, setHosts] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [selectedHost, setSelectedHost] = useState(null); const [wizardOpen, setWizardOpen] = useState(false); const timerRef = useRef | null>(null); // 远程命令 const [cmdOpen, setCmdOpen] = useState(false); const [cmdHost, setCmdHost] = useState(''); const [cmdCommand, setCmdCommand] = useState(''); const [cmdTimeout, setCmdTimeout] = useState(30); const [cmdSubmitting, setCmdSubmitting] = useState(false); const [cmdResult, setCmdResult] = useState<{ status: string; result?: string; output?: string; error?: string } | null>(null); const loadLatest = useCallback(async (silent = false) => { if (!silent) setRefreshing(true); try { const data = await agentApi.getLatest(); const list = asList(data) as AgentHostRecord[]; setHosts(list); // 同步选中主机的最新数据 if (selectedHost) { const fresh = list.find((h) => h.hostname === selectedHost.hostname); if (fresh) setSelectedHost(fresh); } } catch (err) { if (!silent) toast.error('加载主机列表失败'); console.error(err); } finally { setLoading(false); setRefreshing(false); } }, [selectedHost]); useEffect(() => { loadLatest(); timerRef.current = setInterval(() => loadLatest(true), 30000); return () => { if (timerRef.current) clearInterval(timerRef.current); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // 打开远程命令弹窗 const openCommandDialog = (hostname: string) => { setCmdHost(hostname); setCmdCommand(''); setCmdTimeout(30); setCmdResult(null); setCmdOpen(true); }; // 提交远程命令(直连 SSH 即时返回) const submitCommand = async () => { if (!cmdCommand.trim()) { toast.error('请输入要执行的命令'); return; } setCmdSubmitting(true); setCmdResult(null); try { const res = await agentApi.execCommand({ hostname: cmdHost, command: cmdCommand.trim(), timeout: Number(cmdTimeout) || 30, }) as Record; const resData = (res?.data ?? {}) as Record; setCmdResult({ status: res?.code === 200 ? 'completed' : 'failed', result: (resData.output as string) || (res?.message as string) || '无输出', error: resData.error as string | undefined, }); } catch (err) { setCmdResult({ status: 'failed', result: err?.message || '执行失败' }); } finally { setCmdSubmitting(false); loadLatest(true); } }; const closeCommandDialog = () => { setCmdOpen(false); }; const onlineCount = hosts.filter((h) => isOnline(h.last_report || h.last_reported_at || h.updated_at)).length; const stats = [ { label: '主机总数', value: hosts.length, icon: Server, color: 'dg-text-blue' }, { label: '在线', value: onlineCount, icon: Wifi, color: 'dg-text-green' }, { label: '离线', value: hosts.length - onlineCount, icon: WifiOff, color: 'dg-text-red' }, ]; return (
{/* 页头 */}

Agent 监控

实时监控 Agent 上报的主机指标

{/* 统计卡片 */}
{stats.map((s) => (
{s.value}
{s.label}
))}
{/* 主机列表 */} {loading ? ( 加载中... ) : hosts.length === 0 ? (

暂无 Agent 上报数据

) : (
{hosts.map((host) => { const online = isOnline(host.last_report || host.last_reported_at || host.updated_at); const cpu = Number(host.cpu_percent ?? host.cpu ?? 0); const mem = Number(host.mem_percent ?? host.mem ?? 0); const disk = Number(host.disk_percent ?? host.disk ?? 0); const ports: Record[] = Array.isArray(host.ports) ? (host.ports as Record[]) : []; return ( setSelectedHost(host)} >
{host.hostname}
{online ? ( 在线 ) : ( 离线 )}
{/* 指标条 */}
{/* 端口 & 时间 */}
{ports.length} 个端口 {timeAgo(host.last_report || host.last_reported_at || host.updated_at)}
{/* 操作 */}
e.stopPropagation()}>
); })}
)} {/* 详情抽屉 */} setSelectedHost(null)} onCommand={(hn) => openCommandDialog(hn)} /> {/* 远程命令弹窗 */} { if (!v) closeCommandDialog(); }}> 远程命令 在 {cmdHost} 上执行命令
setCmdHost(e.target.value)} readOnly className="h-9 dg-input" />