diff --git a/frontend/src/components/agent/AgentMonitor.tsx b/frontend/src/components/agent/AgentMonitor.tsx index bb3919b..550972d 100644 --- a/frontend/src/components/agent/AgentMonitor.tsx +++ b/frontend/src/components/agent/AgentMonitor.tsx @@ -2,6 +2,8 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { toast } from 'sonner'; import { agentApi } from '@/services/api'; import { AgentWizard } from './AgentWizard'; +import { HostDetailDialog } from './HostDetailDialog'; +import { isOnline, timeAgo, asList } from './agentUtils'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -39,45 +41,6 @@ import { Clock, Server, } from 'lucide-react'; -import { - AreaChart, - Area, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, -} from 'recharts'; - -// ============ 工具函数 ============ - -/** 在线判定:3 分钟内有上报视为在线 */ -function isOnline(lastReport: any): boolean { - if (!lastReport) return false; - const t = new Date(lastReport).getTime(); - if (Number.isNaN(t)) return false; - return Date.now() - t < 3 * 60 * 1000; -} - -function timeAgo(ts: any): string { - if (!ts) return '--'; - const t = new Date(ts).getTime(); - if (Number.isNaN(t)) return '--'; - const diff = Math.floor((Date.now() - t) / 1000); - if (diff < 60) return `${diff}秒前`; - if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`; - if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`; - return new Date(ts).toLocaleString(); -} - -function asList(data: any): any[] { - if (!data) return []; - if (Array.isArray(data)) return data; - if (Array.isArray(data.list)) return data.list; - if (Array.isArray(data.items)) return data.items; - if (Array.isArray(data.data)) return data.data; - return []; -} // ============ 组件 ============ @@ -497,176 +460,4 @@ function MetricBar({ icon: Icon, label, value }: { icon: any; label: string; val ); } -// ============ 主机详情弹窗 ============ -function HostDetailDialog({ - host, - onClose, - onCommand, -}: { - host: any; - onClose: () => void; - onCommand: (hostname: string) => void; -}) { - const [history, setHistory] = useState([]); - const [loading, setLoading] = useState(false); - const [hours, setHours] = useState(1); - - useEffect(() => { - if (!host) return; - let active = true; - setLoading(true); - agentApi - .getHistory(host.hostname, hours) - .then((data) => { - if (active) setHistory(asList(data)); - }) - .catch(() => { if (active) setHistory([]); }) - .finally(() => { if (active) setLoading(false); }); - return () => { active = false; }; - }, [host, hours]); - - if (!host) return null; - - 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: any[] = Array.isArray(host.ports) ? host.ports : []; - - const chartData = history.map((h) => ({ - time: new Date(h.timestamp || h.time || h.created_at || Date.now()).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }), - cpu: Number(h.cpu_percent ?? h.cpu ?? 0), - mem: Number(h.mem_percent ?? h.mem ?? 0), - disk: Number(h.disk_percent ?? h.disk ?? 0), - })); - - return ( - { if (!v) onClose(); }}> - - - - - {host.hostname} - {online ? ( - 在线 - ) : ( - 离线 - )} - - 查看主机详情和指标趋势 - - -
- {/* 基本信息 */} -
- - - - -
- - {/* 趋势图 */} -
-
- - - 指标趋势 - - -
-
- {loading ? ( -
- -
- ) : chartData.length === 0 ? ( -
暂无历史数据
- ) : ( - - - - - - - - - - - - - - - - - - - - - - - - - - )} -
-
- - {/* 端口列表 */} -
- - - 端口发现 ({ports.length}) - - {ports.length === 0 ? ( -

暂无端口数据

- ) : ( -
- {ports.map((p, idx) => ( - - - {p.port ?? p} - - ))} -
- )} -
-
- - - - - -
-
- ); -} - -function InfoCell({ label, value }: { label: string; value: string }) { - return ( -
-
{label}
-
{value}
-
- ); -} - export default AgentMonitor; diff --git a/frontend/src/components/agent/HostDetailDialog.tsx b/frontend/src/components/agent/HostDetailDialog.tsx new file mode 100644 index 0000000..d6ca309 --- /dev/null +++ b/frontend/src/components/agent/HostDetailDialog.tsx @@ -0,0 +1,201 @@ +// 主机详情弹窗:展示历史指标趋势图 + 端口发现列表(从 AgentMonitor.tsx 提取) // Logan.Li +import { useState, useEffect } from 'react'; +import { agentApi } from '@/services/api'; +import { isOnline, timeAgo, asList } from './agentUtils'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Server, Activity, Network, Terminal, Loader2 } from 'lucide-react'; +import { + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from 'recharts'; + +export function HostDetailDialog({ + host, + onClose, + onCommand, +}: { + host: any; + onClose: () => void; + onCommand: (hostname: string) => void; +}) { + const [history, setHistory] = useState([]); + const [loading, setLoading] = useState(false); + const [hours, setHours] = useState(1); + + useEffect(() => { + if (!host) return; + let active = true; + setLoading(true); + agentApi + .getHistory(host.hostname, hours) + .then((data) => { + if (active) setHistory(asList(data)); + }) + .catch(() => { if (active) setHistory([]); }) + .finally(() => { if (active) setLoading(false); }); + return () => { active = false; }; + }, [host, hours]); + + if (!host) return null; + + const online = isOnline(host.last_report || host.last_reported_at || host.updated_at); + const ports: any[] = Array.isArray(host.ports) ? host.ports : []; + + const chartData = history.map((h) => ({ + time: new Date(h.timestamp || h.time || h.created_at || Date.now()).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }), + cpu: Number(h.cpu_percent ?? h.cpu ?? 0), + mem: Number(h.mem_percent ?? h.mem ?? 0), + disk: Number(h.disk_percent ?? h.disk ?? 0), + })); + + return ( + { if (!v) onClose(); }}> + + + + + {host.hostname} + {online ? ( + 在线 + ) : ( + 离线 + )} + + 查看主机详情和指标趋势 + + +
+ {/* 基本信息 */} +
+ + + + +
+ + {/* 趋势图 */} +
+
+ + + 指标趋势 + + +
+
+ {loading ? ( +
+ +
+ ) : chartData.length === 0 ? ( +
暂无历史数据
+ ) : ( + + + + + + + + + + + + + + + + + + + + + + + + + + )} +
+
+ + {/* 端口列表 */} +
+ + + 端口发现 ({ports.length}) + + {ports.length === 0 ? ( +

暂无端口数据

+ ) : ( +
+ {ports.map((p, idx) => ( + + + {p.port ?? p} + + ))} +
+ )} +
+
+ + + + + +
+
+ ); +} + +function InfoCell({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
{value}
+
+ ); +} + +export default HostDetailDialog; diff --git a/frontend/src/components/agent/agentUtils.ts b/frontend/src/components/agent/agentUtils.ts new file mode 100644 index 0000000..da5149c --- /dev/null +++ b/frontend/src/components/agent/agentUtils.ts @@ -0,0 +1,31 @@ +// Agent 监控相关工具函数(从 AgentMonitor.tsx 提取,供多组件共享) // Logan.Li + +/** 在线判定:3 分钟内有上报视为在线 */ +export function isOnline(lastReport: any): boolean { + if (!lastReport) return false; + const t = new Date(lastReport).getTime(); + if (Number.isNaN(t)) return false; + return Date.now() - t < 3 * 60 * 1000; +} + +/** 相对时间格式化("3分钟前" / "2小时前") */ +export function timeAgo(ts: any): string { + if (!ts) return '--'; + const t = new Date(ts).getTime(); + if (Number.isNaN(t)) return '--'; + const diff = Math.floor((Date.now() - t) / 1000); + if (diff < 60) return `${diff}秒前`; + if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`; + if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`; + return new Date(ts).toLocaleString(); +} + +/** 兼容多种列表响应包装:数组 / {list} / {items} / {data} */ +export function asList(data: any): any[] { + if (!data) return []; + if (Array.isArray(data)) return data; + if (Array.isArray(data.list)) return data.list; + if (Array.isArray(data.items)) return data.items; + if (Array.isArray(data.data)) return data.data; + return []; +}