From 228b6b52f95b55c6c7016d0ae300c113869ab36d Mon Sep 17 00:00:00 2001 From: attacker Date: Fri, 19 Jun 2026 01:15:42 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20HostMonitor=20Brush=E5=9B=BE=E8=A1=A8?= =?UTF-8?q?=E7=BC=A9=E6=94=BE=20+=20avg/max/min=E4=B8=8B=E6=8B=89=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 BucketPoint 结构 (avg/max/min/count) - 时间范围切换自动调整聚合窗口 (1h→60s, 6h→5min, 12h→10min...) - 每张指标卡片支持平均值/最大值/最小值切换 - Recharts Brush 组件实现鼠标拖选时间范围缩放 - 参考线显示整体均值 - 图表高度增大到 48 适配 Brush 条 - 磁盘分区列表兼容 agent 上报的原始字段名 --- frontend/src/pages/HostMonitor.tsx | 308 +++++++++++++++++------------ 1 file changed, 180 insertions(+), 128 deletions(-) diff --git a/frontend/src/pages/HostMonitor.tsx b/frontend/src/pages/HostMonitor.tsx index e6d25a9..0a7ac78 100644 --- a/frontend/src/pages/HostMonitor.tsx +++ b/frontend/src/pages/HostMonitor.tsx @@ -1,4 +1,4 @@ -// 主机监控详情 — 阿里云云监控风格 (分类指标 + 时间范围 + 时序图表) +// 主机监控详情 — 阿里云云监控风格 (Brush缩放 + avg/max/min选择器) import { useState, useEffect, useCallback, useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import { agentApi } from '@/services/api'; @@ -12,29 +12,52 @@ import { Info, Monitor, Disc, Zap, Clock } from 'lucide-react'; import { - AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line, + AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, + ResponsiveContainer, Brush, ReferenceLine, } from 'recharts'; -// 时间范围配置 +// 时间范围 → 聚合窗口 → 目标桶数 const TIME_RANGES = [ - { label: '1小时', hours: 1 }, - { label: '6小时', hours: 6 }, - { label: '12小时', hours: 12 }, - { label: '1天', hours: 24 }, - { label: '3天', hours: 72 }, - { label: '7天', hours: 168 }, - { label: '14天', hours: 336 }, + { label: '1小时', hours: 1, window: 60, buckets: 60 }, + { label: '6小时', hours: 6, window: 300, buckets: 72 }, + { label: '12小时', hours: 12, window: 600, buckets: 72 }, + { label: '1天', hours: 24, window: 1800, buckets: 96 }, + { label: '3天', hours: 72, window: 3600, buckets: 72 }, + { label: '7天', hours: 168, window: 10800, buckets: 56 }, + { label: '14天', hours: 336, window: 21600, buckets: 48 }, ] as const; -// 聚合:把原始数据压缩到 targetBuckets 个桶 -function bucketData(data: number[], targetBuckets: number = 48): { time: string; value: number }[] { - if (!data || data.length === 0) return []; - const perBucket = Math.max(1, Math.ceil(data.length / targetBuckets)); - const result: { time: string; value: number }[] = []; - for (let i = 0; i < data.length; i += perBucket) { - const slice = data.slice(i, i + perBucket); - const avg = slice.reduce((a, b) => a + b, 0) / slice.length; - result.push({ time: `-${Math.round((data.length - i) * 10 / 3600)}h`, value: Number(avg.toFixed(1)) }); +type AggType = 'avg' | 'max' | 'min'; + +interface BucketPoint { + time: string; + timestamp: number; + avg: number; + max: number; + min: number; + count: number; +} + +function bucketWithStats(raw: number[], timestamps: number[], windowSec: number, targetBuckets: number): BucketPoint[] { + if (!raw || raw.length === 0) return []; + const perBucket = Math.max(1, Math.ceil(raw.length / targetBuckets)); + const result: BucketPoint[] = []; + for (let i = 0; i < raw.length; i += perBucket) { + const slice = raw.slice(i, i + perBucket); + const tsSlice = timestamps.slice(i, i + perBucket); + const sum = slice.reduce((a, b) => a + b, 0); + const avg = sum / slice.length; + const max = Math.max(...slice); + const min = Math.min(...slice); + const ts = tsSlice[Math.floor(tsSlice.length / 2)] || 0; + result.push({ + time: new Date(ts * 1000).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }), + timestamp: ts, + avg: Number(avg.toFixed(2)), + max: Number(max.toFixed(2)), + min: Number(min.toFixed(2)), + count: slice.length, + }); } return result.slice(-targetBuckets); } @@ -60,15 +83,14 @@ export function HostMonitor() { const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState('basic'); const [rangeHours, setRangeHours] = useState(1); + const [aggType, setAggType] = useState('avg'); - // 原始历史数据 const [history, setHistory] = useState([]); - - // 最新快照 const [latest, setLatest] = useState(null); - // 服务器信息(来自 asset_servers):OS、内核、总内存等,不与 history 混淆 const [serverInfo, setServerInfo] = useState(null); + const timeConfig = TIME_RANGES.find(tr => tr.hours === rangeHours) || TIME_RANGES[0]; + // 加载主机列表 useEffect(() => { agentApi.getLatest().then((data: any) => { @@ -103,54 +125,56 @@ export function HostMonitor() { .finally(() => setLoading(false)); }, [selectedHost, rangeHours]); - // 提取指标数组 + // 从原始历史数据提取指标 + 分桶聚合 const metrics = useMemo(() => { - const cpu: number[] = []; - const mem: number[] = []; - const load1: number[] = []; - const disk: number[] = []; - const tcpEst: number[] = []; - const rx: number[] = []; - const tx: number[] = []; - const ioRead: number[] = []; - const ioWrite: number[] = []; - const ioReadOps: number[] = []; - const ioWriteOps: number[] = []; + const rawCpu: number[] = []; + const rawMem: number[] = []; + const rawLoad1: number[] = []; + const rawDisk: number[] = []; + const rawTcp: number[] = []; + const rawRx: number[] = []; + const rawTx: number[] = []; + const rawIoR: number[] = []; + const rawIoW: number[] = []; + const rawIoRops: number[] = []; + const rawIoWops: number[] = []; + const timestamps: number[] = []; for (const r of history) { - cpu.push(r.cpu_usage || 0); - mem.push(r.mem_usage || 0); - load1.push(r.load1 || 0); - disk.push(r.disk_usage || 0); + const ts = r.report_time ? new Date(r.report_time).getTime() / 1000 : (r.created_at ? new Date(r.created_at).getTime() / 1000 : 0); + timestamps.push(ts); + + rawCpu.push(r.cpu_usage || 0); + rawMem.push(r.mem_usage || 0); + rawLoad1.push(r.load1 || 0); + rawDisk.push(r.disk_usage || 0); const net = parseJSON(r.net_info); - tcpEst.push(net.tcp_established || 0); - rx.push(net.net_rx_bps || 0); - tx.push(net.net_tx_bps || 0); + rawTcp.push(net.tcp_established || 0); + rawRx.push(net.net_rx_bps || 0); + rawTx.push(net.net_tx_bps || 0); const dio = parseJSON(r.disk_io_info); - ioRead.push(dio.read_bps || 0); - ioWrite.push(dio.write_bps || 0); - ioReadOps.push(dio.read_iops || 0); - ioWriteOps.push(dio.write_iops || 0); + rawIoR.push(dio.read_bps || 0); + rawIoW.push(dio.write_bps || 0); + rawIoRops.push(dio.read_iops || 0); + rawIoWops.push(dio.write_iops || 0); } - const target = rangeHours <= 1 ? 60 : rangeHours <= 6 ? 72 : rangeHours <= 24 ? 96 : 120; - return { - cpu: bucketData(cpu, target), - mem: bucketData(mem, target), - load1: bucketData(load1, target), - disk: bucketData(disk, target), - tcpEst: bucketData(tcpEst, target), - rx: bucketData(rx.map(v => v / 1024), target), // KB/s - tx: bucketData(tx.map(v => v / 1024), target), - ioRead: bucketData(ioRead.map(v => v / 1024), target), // KB/s - ioWrite: bucketData(ioWrite.map(v => v / 1024), target), // KB/s - ioReadOps: bucketData(ioReadOps, target), // count/s - ioWriteOps: bucketData(ioWriteOps, target), // count/s + cpu: bucketWithStats(rawCpu, timestamps, timeConfig.window, timeConfig.buckets), + mem: bucketWithStats(rawMem, timestamps, timeConfig.window, timeConfig.buckets), + load1: bucketWithStats(rawLoad1, timestamps, timeConfig.window, timeConfig.buckets), + disk: bucketWithStats(rawDisk, timestamps, timeConfig.window, timeConfig.buckets), + tcpEst: bucketWithStats(rawTcp, timestamps, timeConfig.window, timeConfig.buckets), + rx: bucketWithStats(rawRx.map(v => v / 1024), timestamps, timeConfig.window, timeConfig.buckets), + tx: bucketWithStats(rawTx.map(v => v / 1024), timestamps, timeConfig.window, timeConfig.buckets), + ioRead: bucketWithStats(rawIoR.map(v => v / 1024), timestamps, timeConfig.window, timeConfig.buckets), + ioWrite: bucketWithStats(rawIoW.map(v => v / 1024), timestamps, timeConfig.window, timeConfig.buckets), + ioReadOps: bucketWithStats(rawIoRops, timestamps, timeConfig.window, timeConfig.buckets), + ioWriteOps: bucketWithStats(rawIoWops, timestamps, timeConfig.window, timeConfig.buckets), }; - }, [history, rangeHours]); + }, [history, timeConfig]); const netInfo = parseJSON(latest?.net_info); const portInfo: any[] = Array.isArray(latest?.port_info) ? latest.port_info : (Array.isArray(parseJSON(latest?.port_info)) ? parseJSON(latest?.port_info) : []); @@ -178,7 +202,7 @@ export function HostMonitor() { return (
- {/* 顶栏:主机选择 + 时间范围 */} + {/* 顶栏 */}
@@ -199,20 +223,34 @@ export function HostMonitor() { )}
- {/* 时间范围 */} -
- {TIME_RANGES.map(tr => ( - - ))} - 周期: 15秒 +
+ {/* 聚合类型选择 */} + + + {/* 时间范围 */} +
+ {TIME_RANGES.map(tr => ( + + ))} +
+ 聚合: {aggType}
@@ -220,10 +258,10 @@ export function HostMonitor() { {[ - { id: 'basic', label: '基础监控', icon: , desc: 'CPU/内存/负载' }, - { id: 'disk', label: '磁盘监控', icon: , desc: '使用率/IO' }, - { id: 'network', label: '网络监控', icon: , desc: 'TCP/流量' }, - { id: 'system', label: '系统信息', icon: , desc: 'OS/内核/运行时间' }, + { id: 'basic', label: '基础监控', icon: }, + { id: 'disk', label: '磁盘监控', icon: }, + { id: 'network', label: '网络监控', icon: }, + { id: 'system', label: '系统信息', icon: }, ].map(tab => ( {tab.icon}{tab.label} @@ -231,78 +269,67 @@ export function HostMonitor() { ))} - {/* ═══ 基础监控: CPU / 内存 / 负载 ═══ */} + {/* 基础监控 */} } - data={metrics.cpu} color="#3b82f6" unit="%" domain={[0, 100]} - period={`周期: ${rangeHours <= 1 ? '15' : rangeHours <= 6 ? '1m' : '5m'}秒`} + data={metrics.cpu} color="#3b82f6" unit="%" domain={[0, 100]} aggType={aggType} /> } - data={metrics.mem} color="#10b981" unit="%" domain={[0, 100]} - period={`周期: ${rangeHours <= 1 ? '15' : rangeHours <= 6 ? '1m' : '5m'}秒`} + data={metrics.mem} color="#10b981" unit="%" domain={[0, 100]} aggType={aggType} /> } - data={metrics.load1} color="#8b5cf6" unit="" domain={[0, 'auto']} - period={`周期: ${rangeHours <= 1 ? '15' : rangeHours <= 6 ? '1m' : '5m'}秒`} + data={metrics.load1} color="#8b5cf6" unit="" domain={[0, 'auto']} aggType={aggType} /> - {/* ═══ 磁盘监控: 使用率 / BPS / IOPS ═══ */} + {/* 磁盘监控 */}
} - data={metrics.disk} color="#f97316" unit="%" domain={[0, 100]} - period={`周期: ${rangeHours <= 1 ? '15' : rangeHours <= 6 ? '1m' : '5m'}秒`} + data={metrics.disk} color="#f97316" unit="%" domain={[0, 100]} aggType={aggType} /> } - data={metrics.ioRead} color="#3b82f6" unit="KB/s" domain={[0, 'auto']} - period="平均值" + title="所有磁盘读取BPS(KB/s)" value={currentVal('ioR')} icon={} + data={metrics.ioRead} color="#3b82f6" unit="KB/s" domain={[0, 'auto']} aggType={aggType} /> } - data={metrics.ioWrite} color="#10b981" unit="KB/s" domain={[0, 'auto']} - period="平均值" + title="所有磁盘写入BPS(KB/s)" value={currentVal('ioW')} icon={} + data={metrics.ioWrite} color="#10b981" unit="KB/s" domain={[0, 'auto']} aggType={aggType} />
} - data={metrics.ioReadOps} color="#6366f1" unit="count/s" domain={[0, 'auto']} - period="平均值" + data={metrics.ioReadOps} color="#6366f1" unit="count/s" domain={[0, 'auto']} aggType={aggType} /> } - data={metrics.ioWriteOps} color="#22d3ee" unit="count/s" domain={[0, 'auto']} - period="平均值" + data={metrics.ioWriteOps} color="#22d3ee" unit="count/s" domain={[0, 'auto']} aggType={aggType} />
- {/* ═══ 网络监控: TCP连接 / 接收 / 发送 ═══ */} + {/* 网络监控 */} } - data={metrics.tcpEst} color="#8b5cf6" unit="个" domain={[0, 'auto']} - period={`ESTABLISHED / TIME_WAIT ${netInfo.tcp_time_wait || 0}`} + data={metrics.tcpEst} color="#8b5cf6" unit="个" domain={[0, 'auto']} aggType={aggType} /> } - data={metrics.rx} color="#3b82f6" unit="KB/s" domain={[0, 'auto']} - period={netInfo.nic_name ? `网卡: ${netInfo.nic_name}` : '平均值'} + data={metrics.rx} color="#3b82f6" unit="KB/s" domain={[0, 'auto']} aggType={aggType} /> } - data={metrics.tx} color="#10b981" unit="KB/s" domain={[0, 'auto']} - period={netInfo.nic_name ? `网卡: ${netInfo.nic_name}` : '平均值'} + data={metrics.tx} color="#10b981" unit="KB/s" domain={[0, 'auto']} aggType={aggType} /> - {/* ═══ 系统信息 ═══ */} + {/* 系统信息 */} @@ -346,10 +373,10 @@ export function HostMonitor() { )} - {/* 磁盘分区详情 */} + {/* 磁盘分区 */} {latest?.disk_info && (() => { const disk = parseJSON(latest.disk_info); - const parts = disk.partitions || []; + const parts = Array.isArray(disk) ? disk : (disk.partitions || []); if (!parts.length) return null; return ( @@ -361,12 +388,12 @@ export function HostMonitor() {
{parts.map((p: any, i: number) => (
- {p.mountpoint || p.path} + {p.mount || p.mountpoint || p.device || `disk${i}`}
-
+
- {p.used || '?'}/{p.total || '?'} - {p.used_percent || 0}% + {p.used_gb?.toFixed(1) || p.used || '?'}/{p.total_gb?.toFixed(0) || p.total || '?'}GB + {p.usage_percent || p.used_percent || 0}%
))}
@@ -375,7 +402,7 @@ export function HostMonitor() { ); })()} - {/* 网络接口详情 */} + {/* 网络接口 */} {netInfo && ( @@ -387,7 +414,7 @@ export function HostMonitor() {
网卡:{netInfo.nic_name || '—'}
接收:{netInfo.net_rx_bps ? `${(netInfo.net_rx_bps/1024).toFixed(1)} KB/s` : '—'}
发送:{netInfo.net_tx_bps ? `${(netInfo.net_tx_bps/1024).toFixed(1)} KB/s` : '—'}
-
EST/TW/LN:{netInfo.tcp_established || 0}/{netInfo.tcp_time_wait || 0}/{netInfo.tcp_listen || 0}
+
EST/TW:{netInfo.tcp_established || 0}/{netInfo.tcp_time_wait || 0}
@@ -398,13 +425,20 @@ export function HostMonitor() { ); } -// ═══════ 指标图表卡片 ═══════ +// ═══════ 指标图表卡片 (带 Brush 缩放 + avg/max/min 数据) ═══════ function MetricChartCard({ - title, value, icon, data, color, unit, domain, period, + title, value, icon, data, color, unit, domain, aggType, }: { - title: string; value: string; icon: React.ReactNode; data: { time: string; value: number }[]; - color: string; unit: string; domain: [number, number | 'auto']; period: string; + title: string; value: string; icon: React.ReactNode; + data: BucketPoint[]; color: string; unit: string; + domain: [number, number | 'auto']; aggType: AggType; }) { + // 选择当前聚合类型的值 + const chartData = data.map(d => ({ + ...d, + value: d[aggType], + })); + return ( @@ -414,7 +448,9 @@ function MetricChartCard({ {icon} {title}
- {period} + + {aggType === 'avg' ? '平均值' : aggType === 'max' ? '最大值' : '最小值'} +
{/* 当前值 */} @@ -423,15 +459,15 @@ function MetricChartCard({ {unit && {unit}}
- {/* 趋势图 */} -
- {data.length === 0 ? ( + {/* 趋势图 + Brush 缩放 */} +
+ {chartData.length === 0 ? (
暂无数据
) : ( - + @@ -440,18 +476,34 @@ function MetricChartCard({ - max * 1.1] : domain as [number, number]} - /> + max * 1.1] : domain as [number, number]} /> [`${v.toFixed(2)} ${unit}`, title.replace(/\(.+\)/, '')]} + labelFormatter={(label) => `时间: ${label}`} /> + {/* Brush 缩放条 — 可拖动选择时间范围,鼠标滚轮精细调整 */} + t} + startIndex={Math.max(0, chartData.length - Math.min(20, chartData.length))} + /> + {/* 参考线:平均值 */} + {chartData.length > 0 && ( + s + d.value, 0) / chartData.length} + stroke={color} + strokeOpacity={0.3} + strokeDasharray="4 4" + /> + )} )}