Files
aiops/frontend/src/pages/monitor/HostMonitor.tsx
T
attacker a9513c2056 feat(i18n): 全量中英文国际化改造
- 翻译 en-US.json 中 1006 条未翻译的中文条目为英文
- 提取 30+ 组件文件的硬编码中文到 locale JSON
- 新增 900+ 翻译键覆盖所有业务模块:
  - Layout (Sidebar, Header, Breadcrumb)
  - Server (ServerForm, CloudServerImport, WebShell, RdpTools)
  - Deploy (ReleaseManager, PipelineCreate, AppRelease)
  - Kubernetes (DeploymentEdit, PodExec, PodLogs, ResourceEditor)
  - Monitor (UnifiedQueryPanel, AlertCenter, Prometheus/Grafana Dashboard)
  - AIOps (predictUtils, PredictiveCostTab, AIPredictive)
  - System (useLdapManagement, AgentMonitor, AgentWizard)
- tsc --noEmit 类型检查通过
2026-07-04 03:42:27 +08:00

548 lines
31 KiB
TypeScript

// 主机监控详情 — 阿里云云监控风格 + 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<HostMonitorTimeRange>(DEFAULT_TIME_RANGE);
const [aggType, setAggType] = useState<AggType>('avg');
const [portPolicyMode, setPortPolicyMode] = useState('whitelist');
const [portWhitelist, setPortWhitelist] = useState<number[]>([]);
const [portPolicyDialogOpen, setPortPolicyDialogOpen] = useState(false);
const [portPolicySaving, setPortPolicySaving] = useState(false);
const fdChartRef = useRef<ZoomableChartHandle>(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 {
// 保存失败时保持弹窗状态,错误由调用方统一处理。// Logan.Li
} 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 = (
<div className="flex items-center justify-between flex-wrap gap-2 w-full">
<div className="flex items-center gap-2">
<Select value={selectedHost} onValueChange={setSelectedHost}>
<SelectTrigger className="w-52 h-8 text-xs dg-input">
<SelectValue placeholder={i18n.t('monitor.hostmonitortsx.sb6637d99')} />
</SelectTrigger>
<SelectContent className="dg-input">
{hosts.map((h: JsonData) => (
<SelectItem key={h.hostname} value={h.hostname}>{h.hostname}</SelectItem>
))}
</SelectContent>
</Select>
{serverInfo && (
<span className="text-xs dg-text-muted">
{String(serverInfo.os || '')}{serverInfo.cpu_cores ? ` | ${serverInfo.cpu_cores as number | string} ${i18n.t('monitor.hostMonitorPanel.cores')}` : ''}{serverInfo.mem_total ? ` | ${(serverInfo.mem_total as number).toFixed(0)}GB` : ''}
</span>
)}
</div>
<div className="flex items-center gap-2">
<Select value={aggType} onValueChange={(v) => setAggType(v as AggType)}>
<SelectTrigger className="w-24 h-7 text-xs dg-input">
<SelectValue />
</SelectTrigger>
<SelectContent className="dg-input">
<SelectItem value="avg"> {i18n.t('monitor.hostmonitortsx.se68632a3')} </SelectItem>
<SelectItem value="max"> {i18n.t('monitor.hostmonitortsx.s5da89314')} </SelectItem>
<SelectItem value="min"> {i18n.t('monitor.hostmonitortsx.sc322edb8')} </SelectItem>
</SelectContent>
</Select>
<div className="flex flex-col items-end gap-0.5">
<HostTimeRangePicker value={timeRange} onChange={setTimeRange} />
{actualDataSpan && (
<span className="text-[10px] dg-text-muted tabular-nums">{i18n.t('monitor.hostMonitorPanel.dataSpan', { span: actualDataSpan })}</span>
)}
</div>
</div>
</div>
);
const monitorBody = (
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="h-8 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] gap-0 overflow-x-auto">
{[
{ id: 'basic', label: i18n.t('monitor.hostmonitortsx.s00303869'), icon: <Monitor className="w-3.5 h-3.5" /> },
{ id: 'disk', label: i18n.t('monitor.hostmonitortsx.s5dd0a090'), icon: <HardDrive className="w-3.5 h-3.5" /> },
{ id: 'network', label: i18n.t('monitor.hostmonitortsx.s59805cc8'), icon: <Network className="w-3.5 h-3.5" /> },
{ id: 'ports', label: i18n.t('agent.portMonitor'), icon: <Server className="w-3.5 h-3.5" /> },
{ id: 'process', label: i18n.t('monitor.hostProcess'), icon: <List className="w-3.5 h-3.5" /> },
{ id: 'system', label: i18n.t('system.systemInfo'), icon: <Info className="w-3.5 h-3.5" /> },
{ id: 'health_probes', label: i18n.t('kubernetes.container.probes'), icon: <HeartPulse className="w-3.5 h-3.5" /> },
].map(tab => (
<TabsTrigger key={tab.id} value={tab.id} className="text-[11px] h-7 gap-1.5 px-3 data-[state=active]:bg-[var(--dg-bg-primary)]">
<span className="shrink-0 opacity-80">{tab.icon}</span>
{tab.label}
</TabsTrigger>
))}
</TabsList>
<TabsContent value="basic" className="mt-3 grid grid-cols-1 md:grid-cols-3 gap-3">
<MetricChartCard title={i18n.t('monitor.hostmonitortsx.s4e6cbf10')} value={currentVal('cpu')} icon={<Cpu />} iconTone="blue"
data={metrics.cpu} color="#3b82f6" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} />
<MetricChartCard title={i18n.t('monitor.hostmonitortsx.s79f80fbf')} value={latest?.cpu_iowait != null ? `${(latest.cpu_iowait as number).toFixed(1)}%` : '--'} icon={<Clock />} iconTone="orange"
data={metrics.iowait} color="#f97316" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} />
<MetricChartCard title={i18n.t('monitor.hostmonitortsx.s78262b59')} value={currentVal('mem')} icon={<Server />} iconTone="green"
data={metrics.mem} color="#10b981" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} />
<MetricChartCard title={i18n.t('monitor.hostmonitortsx.sa23fcda0')} value={latest?.mem_available != null ? `${(latest.mem_available as number).toFixed(1)}GB` : '--'} icon={<Server />} iconTone="green"
data={metrics.memAvail} color="#34d399" unit="GB" domain={[0, 'auto']} aggType={aggType} />
<MetricChartCard title={i18n.t('monitor.hostmonitortsx.s05589bfd')} value={currentVal('swap')} icon={<HardDrive />} iconTone="red"
data={metrics.swap} color="#ef4444" unit="%" domain={[0, 100]} aggType={aggType} />
<MetricChartCard title={i18n.t('monitor.hostmonitortsx.sae62efb2')} value={currentVal('load1')} icon={<Activity />} iconTone="purple"
data={metrics.load1} color="#8b5cf6" unit="" domain={[0, 'auto']} aggType={aggType} />
</TabsContent>
<TabsContent value="disk" className="mt-3 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<MetricChartCard title={i18n.t('monitor.hostmonitortsx.sdf969221')} value={currentVal('disk')} icon={<HardDrive />} iconTone="orange"
data={metrics.disk} color="#f97316" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} />
<MetricChartCard title={i18n.t('monitor.hostmonitortsx.s20296121')} value={(() => {
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={<HardDrive />} iconTone="purple"
data={metrics.inode} color="#8b5cf6" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} />
<MetricChartCard title={i18n.t('monitor.hostmonitortsx.s72e3c273')} value={currentVal('ioR')} icon={<Disc />} iconTone="blue"
data={metrics.ioRead} color="#3b82f6" unit="KB/s" domain={[0, 'auto']} aggType={aggType} />
<MetricChartCard title={i18n.t('monitor.hostmonitortsx.sa9b96f9b')} value={currentVal('ioW')} icon={<Disc />} iconTone="green"
data={metrics.ioWrite} color="#10b981" unit="KB/s" domain={[0, 'auto']} aggType={aggType} />
<MetricChartCard title={i18n.t('monitor.hostmonitortsx.s996adabd')} value={diskIOInfo?.util != null ? `${diskIOInfo.util.toFixed(1)}%` : '--'} icon={<Disc />} iconTone="red"
data={metrics.ioUtil} color="#ef4444" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} />
</div>
</TabsContent>
<TabsContent value="network" className="mt-3 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<MetricChartCard title="TCP EST" value={currentVal('tcp')} icon={<Network />} iconTone="purple"
data={metrics.tcpEst} color="#8b5cf6" unit={i18n.t('monitor.hostMonitorPanel.portUnit')} domain={[0, 'auto']} aggType={aggType} />
<MetricChartCard title="TCP TIME_WAIT" value={currentVal('tw')} icon={<Clock />} iconTone="yellow"
data={metrics.tcpTW} color="#f59e0b" unit="个" domain={[0, 'auto']} aggType={aggType} />
<MetricChartCard title="TCP CLOSE_WAIT" value={netInfo?.tcp_close_wait != null ? String(netInfo.tcp_close_wait) : '--'} icon={<Clock />} iconTone="red"
data={metrics.tcpCW} color="#ef4444" unit="个" domain={[0, 'auto']} aggType={aggType} />
<MetricChartCard title={i18n.t('monitor.hostmonitortsx.s56c1b42a')} value={netInfo ? String((netInfo.tcp_established||0)+(netInfo.tcp_time_wait||0)+(netInfo.tcp_close_wait||0)) : '--'} icon={<Network />} iconTone="cyan"
data={metrics.connTotal} color="#22d3ee" unit="个" domain={[0, 'auto']} aggType={aggType} />
</div>
<DualLineChart
title={i18n.t('monitor.hostmonitortsx.s1e2a9e41')}
currentRx={netInfo?.net_rx_bps != null ? formatBPS(netInfo.net_rx_bps) : '--'}
currentTx={netInfo?.net_tx_bps != null ? formatBPS(netInfo.net_tx_bps) : '--'}
rxData={metrics.rx} txData={metrics.tx}
aggType={aggType} unit="KB/s"
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
<MetricChartCard title={i18n.t('monitor.hostmonitortsx.s9f7e9902')} value={netInfo?.net_rx_errors != null ? String(netInfo.net_rx_errors) : '--'} icon={<XCircle />} iconTone="red"
data={metrics.rxErr} color="#ef4444" unit="个" domain={[0, 'auto']} aggType={aggType} />
<MetricChartCard title={i18n.t('monitor.hostmonitortsx.sb2ffd58e')} value={netInfo?.net_tx_errors != null ? String(netInfo.net_tx_errors) : '--'} icon={<XCircle />} iconTone="orange"
data={metrics.txErr} color="#f97316" unit="个" domain={[0, 'auto']} aggType={aggType} />
</div>
</TabsContent>
<TabsContent value="ports" className="mt-3 space-y-3">
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<h3 className="text-xs font-medium text-[var(--dg-text-primary)] mb-3 flex items-center gap-2">
<Server className="h-4 w-4 dg-text-green" />监听端口 {portInfo.length}
<span className="ml-auto flex items-center gap-2">
{portPolicyMode === 'blacklist' && portWhitelist.length > 0 && (
<Badge variant="outline" className="text-[10px] dg-text-yellow border-amber-500/30">
黑名单: {portWhitelist.join(',')}
</Badge>
)}
{portPolicyMode === 'whitelist' && (
<Badge variant="outline" className="text-[10px] dg-text-blue border-blue-500/30">
白名单模式
</Badge>
)}
<Button variant="outline" size="sm" className="h-6 text-[10px] px-2"
onClick={() => { refetchPortPolicy(); setPortPolicyDialogOpen(true); }}>
策略配置
</Button>
</span>
</h3>
{portInfo.length === 0 ? (
<div className="text-center py-6 text-xs text-[var(--dg-text-muted)] border border-dashed border-[var(--dg-border)] rounded-lg">
暂无端口数据
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="text-[var(--dg-text-muted)] border-b border-[var(--dg-border)]">
<th className="py-1.5 text-left font-medium"> {i18n.t('system.ldapPort')} </th>
<th className="py-1.5 text-left font-medium"> {i18n.t('kubernetes.service.protocol')} </th>
<th className="py-1.5 text-left font-medium"> {i18n.t('monitor.hostmonitortsx.sf88522cf')} </th>
<th className="py-1.5 text-right font-medium"> {i18n.t('system.terminalStatus')} </th>
</tr>
</thead>
<tbody>
{portInfo.map((p: JsonData, i: number) => (
<tr key={i} className="border-b border-[var(--dg-border)]/30 hover:bg-[var(--dg-bg-hover)] transition-colors">
<td className="py-1.5 font-mono text-[var(--dg-text-primary)]">{p.port}</td>
<td className="py-1.5">
<Badge variant="outline" className="text-[10px] font-mono">
{p.protocol || 'tcp'}
</Badge>
</td>
<td className="py-1.5 font-mono text-[var(--dg-text-muted)]">{p.process || '—'}</td>
<td className="py-1.5 text-right">
<Badge variant="default" className="text-[10px] bg-emerald-500/20 dg-text-green border-emerald-500/30">
LISTEN
</Badge>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="process" className="mt-3 space-y-3">
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<h3 className="text-xs font-medium text-[var(--dg-text-primary)] mb-3 flex items-center gap-2">
<List className="h-4 w-4 dg-text-cyan" />进程 TOP {processTop.length}
</h3>
{processTop.length === 0 ? (
<div className="text-center py-6 text-xs text-[var(--dg-text-muted)] border border-dashed border-[var(--dg-border)] rounded-lg">
Agent v2.0+ 支持,等待数据上报…
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="text-[var(--dg-text-muted)] border-b border-[var(--dg-border)]">
<th className="py-1.5 text-left font-medium w-[40%]"> {i18n.t('monitor.hostmonitortsx.s23ecaf10')} </th>
<th className="py-1.5 text-right font-medium w-[8%]">PID</th>
<th className="py-1.5 text-right font-medium w-[16%]"> {i18n.t('monitor.hostmonitortsx.s78262b59')} </th>
<th className="py-1.5 text-right font-medium w-[16%]">CPU使用率(%)</th>
<th className="py-1.5 text-right font-medium w-[20%]"> {i18n.t('monitor.hostmonitortsx.s8fe5ed35')} </th>
</tr>
</thead>
<tbody>
{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 (
<tr key={i} className="border-b border-[var(--dg-border)]/30 hover:bg-[var(--dg-bg-hover)] transition-colors">
<td className="py-1.5 pr-2">
<div className="flex items-center gap-1 group/cmd">
<span className="font-mono text-[var(--dg-text-primary)] truncate max-w-[320px] inline-block align-middle" title={cmd}>
{cmd}
</span>
<button
className="opacity-0 group-hover/cmd:opacity-100 transition-opacity p-0.5 hover:dg-text-cyan shrink-0"
title={i18n.t('monitor.hostmonitortsx.s92cb88ae')}
onClick={async (e) => {
e.stopPropagation();
try {
await navigator.clipboard.writeText(cmd);
} catch {
const ta = document.createElement('textarea');
ta.value = cmd; ta.style.position = 'fixed'; ta.style.opacity = '0';
document.body.appendChild(ta); ta.select();
document.execCommand('copy'); document.body.removeChild(ta);
}
}}
>
<svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
</button>
</div>
</td>
<td className="py-1.5 text-right font-mono text-[var(--dg-text-muted)]">{p.pid}</td>
<td className="py-1.5 text-right font-mono">
<span className={memp != null && memp > 50 ? 'text-rose-400' : 'dg-text-cyan'}>
{memp != null ? memp.toFixed(2) : '--'}
</span>
</td>
<td className="py-1.5 text-right font-mono">
<span className={cpup != null && cpup > 50 ? 'text-rose-400' : 'text-[var(--dg-text-primary)]'}>
{cpup != null ? cpup.toFixed(2) : '--'}
</span>
</td>
<td className="py-1.5 text-right font-mono text-[var(--dg-text-muted)]">{p.fd_count ?? '--'}</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<div className="flex items-start justify-between gap-2 mb-3">
<h3 className="text-xs font-medium text-[var(--dg-text-primary)] flex items-center gap-2 leading-8">
<MetricIconBox icon={<HardDrive />} tone="purple" />
FD 文件句柄
</h3>
{metrics.fd.length > 0 && (
<ChartToolbar
zoomed={fdChartZoomed}
onExpand={() => fdChartRef.current?.expand()}
onResetZoom={() => fdChartRef.current?.resetZoom()}
/>
)}
</div>
{fdInfo?.allocated != null ? (
<>
<div className="flex items-center gap-4 mb-3 text-xs">
<div>
<span className="text-[var(--dg-text-muted)]"> {i18n.t('deploy.toast.assigned')} </span>
<span className="font-mono text-[var(--dg-text-primary)] font-semibold">{fdInfo.allocated.toLocaleString()}</span>
</div>
<div>
<span className="text-[var(--dg-text-muted)]"> {i18n.t('monitor.metricCatalog.colMax')} </span>
<span className="font-mono text-[var(--dg-text-primary)] font-semibold">{fdInfo.max === -1 ? i18n.t('monitor.hostmonitortsx.sbc436447') : fdInfo.max?.toLocaleString() || '—'}</span>
</div>
<div>
<span className="text-[var(--dg-text-muted)]"> {i18n.t('server.usage')} </span>
<span className={`font-mono font-semibold ${fdInfo.usage_pct > 80 ? 'text-rose-400' : 'dg-text-purple'}`}>
{fdInfo.usage_pct != null ? `${fdInfo.usage_pct.toFixed(1)}%` : '—'}
</span>
</div>
</div>
{metrics.fd.length > 0 && (
<ZoomableAreaChart
ref={fdChartRef}
data={metrics.fd.map((d) => ({ ...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}
/>
)}
</>
) : (
<div className="text-center py-4 text-xs text-[var(--dg-text-muted)]"> {i18n.t('monitor.hostmonitortsx.sae1a30b7')} </div>
)}
</CardContent>
</Card>
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<h3 className="text-xs font-medium text-[var(--dg-text-primary)] mb-3 flex items-center gap-2">
<RefreshCw className="h-4 w-4 dg-text-green" />上下文切换
</h3>
{csInfo?.ctx_per_sec != null ? (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="bg-[var(--dg-bg-primary)] rounded-lg p-3 text-center">
<div className="text-[10px] text-[var(--dg-text-muted)] mb-1">CTX 切换/秒</div>
<div className="text-lg font-bold font-mono dg-text-green">
{(csInfo.ctx_per_sec / 1e6).toFixed(1)}<span className="text-xs font-normal text-[var(--dg-text-muted)] ml-0.5">M</span>
</div>
<div className="text-[10px] text-[var(--dg-text-muted)] mt-1 font-mono">{csInfo.ctx_per_sec?.toLocaleString()} 次</div>
</div>
<div className="bg-[var(--dg-bg-primary)] rounded-lg p-3 text-center">
<div className="text-[10px] text-[var(--dg-text-muted)] mb-1">IRQ 中断/秒</div>
<div className="text-lg font-bold font-mono dg-text-yellow">
{(csInfo.irq_per_sec / 1e6).toFixed(1)}<span className="text-xs font-normal text-[var(--dg-text-muted)] ml-0.5">M</span>
</div>
<div className="text-[10px] text-[var(--dg-text-muted)] mt-1 font-mono">{csInfo.irq_per_sec?.toLocaleString()} 次</div>
</div>
</div>
<div className="space-y-1">
<div className="flex justify-between text-[10px]">
<span className="dg-text-green">CTX</span>
<span className="dg-text-yellow">IRQ</span>
</div>
<div className="h-2 bg-[var(--dg-bg-primary)] rounded-full overflow-hidden flex">
{csInfo.ctx_per_sec + csInfo.irq_per_sec > 0 && (
<>
<div className="h-full bg-emerald-500/60 rounded-l-full transition-all"
style={{ width: `${(csInfo.ctx_per_sec / (csInfo.ctx_per_sec + csInfo.irq_per_sec) * 100).toFixed(1)}%` }} />
<div className="h-full bg-amber-500/60 rounded-r-full transition-all"
style={{ width: `${(csInfo.irq_per_sec / (csInfo.ctx_per_sec + csInfo.irq_per_sec) * 100).toFixed(1)}%` }} />
</>
)}
</div>
</div>
</div>
) : (
<div className="text-center py-4 text-xs text-[var(--dg-text-muted)]"> {i18n.t('monitor.hostmonitortsx.sae1a30b7')} </div>
)}
</CardContent>
</Card>
</div>
</TabsContent>
<SystemInfoSection
selectedHost={selectedHost}
serverInfo={serverInfo}
latest={latest}
heartbeatInfo={heartbeatInfo}
portInfoCount={portInfo.length}
/>
<HealthProbeSection selectedHost={selectedHost} />
</Tabs>
);
const dialogs = (
<PortPolicyDialog
open={portPolicyDialogOpen}
onOpenChange={setPortPolicyDialogOpen}
portPolicyMode={portPolicyMode}
onModeChange={setPortPolicyMode}
portWhitelist={portWhitelist}
onWhitelistChange={setPortWhitelist}
onSave={handleSavePortPolicy}
saving={portPolicySaving}
/>
);
if (embedded) {
return (
<div className="space-y-3">
{toolbar}
{!loading && history.length > 0 && (
<HostCoreMetricsStrip
latest={latest}
netInfo={netInfo}
diskIOInfo={diskIOInfo}
onJump={setActiveTab}
/>
)}
{loading && !history.length ? <PageLoading /> : monitorBody}
{dialogs}
</div>
);
}
return (
<PageShell
variant="workspace"
title={i18n.t('monitor.alertRulesTab.catHost')}
subtitle={selectedHost ? `当前主机: ${selectedHost}` : i18n.t('monitor.hostmonitortsx.s8d493048')}
icon={<Server className="h-5 w-5" />}
toolbar={toolbar}
loading={loading && !history.length}
loadingLabel={i18n.t('monitor.hostmonitortsx.sc23e802e')}
contentClassName="space-y-3"
>
{monitorBody}
{dialogs}
</PageShell>
);
}