// 主机监控详情 — 阿里云云监控风格 + v2.0 新增指标 // 进程TOP / Swap / FD / 上下文切换 // Logan.Li import { useState, useEffect, useMemo, useRef } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; import { agentApi } from '@/api'; import { useHostList, useHostHistory, usePortPolicy } from '@/hooks/api/useHostMonitor'; import { queryKeys } from '@/hooks/api/queryKeys'; import { Card, CardContent } from '@/components/ui/card'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { PageShell } from '@/components/layout/PageShell'; import { PageLoading } from '@/components/layout/PageLoading'; import { Cpu, HardDrive, Network, Activity, Server, Info, Monitor, Disc, Clock, List, HeartPulse, RefreshCw, XCircle, } from 'lucide-react'; import { ZoomableAreaChart, type ZoomableChartHandle } from './host-monitor/ZoomableAreaChart'; import { ChartToolbar } from './host-monitor/ChartToolbar'; import { MetricIconBox } from './host-monitor/MetricIconBox'; import { DEFAULT_TIME_RANGE, type AggType, type HostRecord, type HostMonitorTimeRange } from './host-monitor/types'; import { HostTimeRangePicker } from './host-monitor/HostTimeRangePicker'; import { getRangeSpanMinutes } from './host-monitor/timeRange'; import { parseJSON, formatBPS, buildMetricsFromHistory, getCurrentVal, filterHistoryByRange, getActualDataSpan } from './host-monitor/utils'; import { MetricChartCard } from './host-monitor/MetricChartCard'; import { DualLineChart } from './host-monitor/DualLineChart'; import { HealthProbeSection } from './host-monitor/HealthProbeSection'; import { PortPolicyDialog } from './host-monitor/PortPolicyDialog'; import { SystemInfoSection } from './host-monitor/SystemInfoSection'; import { HostCoreMetricsStrip } from './host-monitor/HostCoreMetricsStrip'; import type { JsonData } from '@/types/common'; import i18n from '@/i18n'; export function HostMonitor({ embedded = false }: { embedded?: boolean }) { const qc = useQueryClient(); const [searchParams] = useSearchParams(); const urlHost = searchParams.get('host') || ''; const [selectedHost, setSelectedHost] = useState(urlHost); const [activeTab, setActiveTab] = useState('basic'); const [timeRange, setTimeRange] = useState(DEFAULT_TIME_RANGE); const [aggType, setAggType] = useState('avg'); const [portPolicyMode, setPortPolicyMode] = useState('whitelist'); const [portWhitelist, setPortWhitelist] = useState([]); const [portPolicyDialogOpen, setPortPolicyDialogOpen] = useState(false); const [portPolicySaving, setPortPolicySaving] = useState(false); const fdChartRef = useRef(null); const [fdChartZoomed, setFdChartZoomed] = useState(false); const { data: hosts = [], isLoading: hostsLoading } = useHostList(); const { data: rawHistory = [], isLoading: historyLoading } = useHostHistory(selectedHost, timeRange); const { data: portPolicy, refetch: refetchPortPolicy } = usePortPolicy(selectedHost); useEffect(() => { if (portPolicy) { setPortPolicyMode(portPolicy.port_filter_mode); setPortWhitelist(portPolicy.port_whitelist); } }, [portPolicy]); useEffect(() => { if (hosts.length > 0 && !selectedHost) { const m = urlHost ? hosts.find((h) => h.hostname === urlHost) : null; setSelectedHost(String(m?.hostname || hosts[0]?.hostname || '')); } }, [hosts, selectedHost, urlHost]); const handleSavePortPolicy = async () => { if (!selectedHost) return; setPortPolicySaving(true); try { await agentApi.savePortPolicy(selectedHost, { port_filter_mode: portPolicyMode, port_whitelist: portWhitelist, }); setPortPolicyDialogOpen(false); qc.invalidateQueries({ queryKey: queryKeys.hostMonitor.portPolicy(selectedHost) }); } catch { } finally { setPortPolicySaving(false); } }; const history = useMemo( () => filterHistoryByRange(rawHistory, timeRange), [rawHistory, timeRange], ); const spanMinutes = getRangeSpanMinutes(timeRange); const timeConfig = useMemo( () => ({ buckets: timeRange.buckets, spanMinutes }), [timeRange.buckets, spanMinutes], ); const actualDataSpan = useMemo( () => getActualDataSpan(history, timeRange), [history, timeRange], ); const loading = hostsLoading || historyLoading; const serverInfo = (hosts.find((h) => h.hostname === selectedHost) || null) as HostRecord | null; const latest = useMemo((): HostRecord => { const base = serverInfo || {}; if (history.length === 0) return base; return { ...base, ...(history[history.length - 1] as HostRecord) }; }, [serverInfo, history]); const metrics = useMemo(() => buildMetricsFromHistory(history, timeConfig), [history, timeConfig]); const netInfo = parseJSON(latest?.net_info); const portInfo: JsonData = Array.isArray(latest?.port_info) ? latest.port_info : (Array.isArray(parseJSON(latest?.port_info)) ? parseJSON(latest?.port_info) : []); const diskIOInfo = parseJSON(latest?.disk_io_info); const swapInfo = parseJSON(latest?.swap_info); const processTop: JsonData = Array.isArray(parseJSON(latest?.process_top_info)) ? parseJSON(latest?.process_top_info) : []; const fdInfo = parseJSON(latest?.fd_info); const csInfo = parseJSON(latest?.cs_info); const heartbeatInfo = parseJSON(latest?.heartbeat_info); const currentVal = (key: string, fallback = '--') => getCurrentVal(latest, netInfo, diskIOInfo, swapInfo, fdInfo, key, fallback); const pctRefLines = [ { value: 80, label: '80%', color: 'var(--dg-yellow)' }, { value: 90, label: '90%', color: 'var(--dg-red)' }, ]; const toolbar = (
{serverInfo && ( {String(serverInfo.os || '')}{serverInfo.cpu_cores ? ` | ${serverInfo.cpu_cores as number | string} 核` : ''}{serverInfo.mem_total ? ` | ${(serverInfo.mem_total as number).toFixed(0)}GB` : ''} )}
{actualDataSpan && ( 数据 {actualDataSpan} )}
); const monitorBody = ( {[ { id: 'basic', label: i18n.t('monitor.hostmonitortsx.s00303869'), icon: }, { id: 'disk', label: i18n.t('monitor.hostmonitortsx.s5dd0a090'), icon: }, { id: 'network', label: i18n.t('monitor.hostmonitortsx.s59805cc8'), icon: }, { id: 'ports', label: i18n.t('agent.portMonitor'), icon: }, { id: 'process', label: i18n.t('monitor.hostProcess'), icon: }, { id: 'system', label: i18n.t('system.systemInfo'), icon: }, { id: 'health_probes', label: i18n.t('kubernetes.container.probes'), icon: }, ].map(tab => ( {tab.icon} {tab.label} ))} } iconTone="blue" data={metrics.cpu} color="#3b82f6" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} /> } iconTone="orange" data={metrics.iowait} color="#f97316" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} /> } iconTone="green" data={metrics.mem} color="#10b981" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} /> } iconTone="green" data={metrics.memAvail} color="#34d399" unit="GB" domain={[0, 'auto']} aggType={aggType} /> } iconTone="red" data={metrics.swap} color="#ef4444" unit="%" domain={[0, 100]} aggType={aggType} /> } iconTone="purple" data={metrics.load1} color="#8b5cf6" unit="" domain={[0, 'auto']} aggType={aggType} />
} iconTone="orange" data={metrics.disk} color="#f97316" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} /> { const dArr = Array.isArray(parseJSON(latest?.disk_info)) ? parseJSON(latest?.disk_info) : []; const root = dArr.find((d: JsonData) => d.mount === '/') || dArr[0]; return root?.inode_usage_pct != null ? `${root.inode_usage_pct.toFixed(1)}%` : '--'; })()} icon={} iconTone="purple" data={metrics.inode} color="#8b5cf6" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} /> } iconTone="blue" data={metrics.ioRead} color="#3b82f6" unit="KB/s" domain={[0, 'auto']} aggType={aggType} /> } iconTone="green" data={metrics.ioWrite} color="#10b981" unit="KB/s" domain={[0, 'auto']} aggType={aggType} /> } iconTone="red" data={metrics.ioUtil} color="#ef4444" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} />
} iconTone="purple" data={metrics.tcpEst} color="#8b5cf6" unit="个" domain={[0, 'auto']} aggType={aggType} /> } iconTone="yellow" data={metrics.tcpTW} color="#f59e0b" unit="个" domain={[0, 'auto']} aggType={aggType} /> } iconTone="red" data={metrics.tcpCW} color="#ef4444" unit="个" domain={[0, 'auto']} aggType={aggType} /> } iconTone="cyan" data={metrics.connTotal} color="#22d3ee" unit="个" domain={[0, 'auto']} aggType={aggType} />
} iconTone="red" data={metrics.rxErr} color="#ef4444" unit="个" domain={[0, 'auto']} aggType={aggType} /> } iconTone="orange" data={metrics.txErr} color="#f97316" unit="个" domain={[0, 'auto']} aggType={aggType} />

监听端口 {portInfo.length} {portPolicyMode === 'blacklist' && portWhitelist.length > 0 && ( 黑名单: {portWhitelist.join(',')} )} {portPolicyMode === 'whitelist' && ( 白名单模式 )}

{portInfo.length === 0 ? (
暂无端口数据
) : (
{portInfo.map((p: JsonData, i: number) => ( ))}
{i18n.t('system.ldapPort')} {i18n.t('kubernetes.service.protocol')} {i18n.t('monitor.hostmonitortsx.sf88522cf')} {i18n.t('system.terminalStatus')}
{p.port} {p.protocol || 'tcp'} {p.process || '—'} LISTEN
)}

进程 TOP {processTop.length}

{processTop.length === 0 ? (
Agent v2.0+ 支持,等待数据上报…
) : (
{processTop.map((p: JsonData, i: number) => { const cmd = p.cmdline || p.name || '?'; const cpup = p.cpu_pct != null ? Number(p.cpu_pct) : null; const memp = p.mem_pct != null ? Number(p.mem_pct) : null; return ( ); })}
{i18n.t('monitor.hostmonitortsx.s23ecaf10')} PID {i18n.t('monitor.hostmonitortsx.s78262b59')} CPU使用率(%) {i18n.t('monitor.hostmonitortsx.s8fe5ed35')}
{cmd}
{p.pid} 50 ? 'text-rose-400' : 'dg-text-cyan'}> {memp != null ? memp.toFixed(2) : '--'} 50 ? 'text-rose-400' : 'text-[var(--dg-text-primary)]'}> {cpup != null ? cpup.toFixed(2) : '--'} {p.fd_count ?? '--'}
)}

} tone="purple" /> FD 文件句柄

{metrics.fd.length > 0 && ( fdChartRef.current?.expand()} onResetZoom={() => fdChartRef.current?.resetZoom()} /> )}
{fdInfo?.allocated != null ? ( <>
{i18n.t('deploy.toast.assigned')} {fdInfo.allocated.toLocaleString()}
{i18n.t('monitor.metricCatalog.colMax')} {fdInfo.max === -1 ? i18n.t('monitor.hostmonitortsx.sbc436447') : fdInfo.max?.toLocaleString() || '—'}
{i18n.t('server.usage')} 80 ? 'text-rose-400' : 'dg-text-purple'}`}> {fdInfo.usage_pct != null ? `${fdInfo.usage_pct.toFixed(1)}%` : '—'}
{metrics.fd.length > 0 && ( ({ ...d, value: d[aggType] }))} series={[{ dataKey: 'value', color: '#a78bfa', label: i18n.t('monitor.hostmonitortsx.s28130dec') }]} yDomain={[0, 'auto']} unit="%" chartId="fd-usage" title={i18n.t('monitor.hostmonitortsx.sc026a1ea')} subtitle={fdInfo?.usage_pct != null ? `当前 ${fdInfo.usage_pct.toFixed(1)}%` : undefined} height={160} toolbarPlacement="header" onZoomedChange={setFdChartZoomed} showHint={false} /> )} ) : (
{i18n.t('monitor.hostmonitortsx.sae1a30b7')}
)}

上下文切换

{csInfo?.ctx_per_sec != null ? (
CTX 切换/秒
{(csInfo.ctx_per_sec / 1e6).toFixed(1)}M
{csInfo.ctx_per_sec?.toLocaleString()} 次
IRQ 中断/秒
{(csInfo.irq_per_sec / 1e6).toFixed(1)}M
{csInfo.irq_per_sec?.toLocaleString()} 次
CTX IRQ
{csInfo.ctx_per_sec + csInfo.irq_per_sec > 0 && ( <>
)}
) : (
{i18n.t('monitor.hostmonitortsx.sae1a30b7')}
)}
); const dialogs = ( ); if (embedded) { return (
{toolbar} {!loading && history.length > 0 && ( )} {loading && !history.length ? : monitorBody} {dialogs}
); } return ( } toolbar={toolbar} loading={loading && !history.length} loadingLabel={i18n.t('monitor.hostmonitortsx.sc23e802e')} contentClassName="space-y-3" > {monitorBody} {dialogs} ); }