Files
aiops/frontend/src/pages/monitor/HostMonitor.tsx
T
attacker 4c4aaf60f6 fix(i18n): U18 幻影 i18n hash 键治理 — 从 git 历史恢复 1540/1696 处原始中文文本 (91%)
问题: i18n 批量转换 (commit 55f216c) 生成了 813 个 hash 键引用但翻译从未写入 JSON,
导致用户在受影响模块看到 hash 键路径(如 monitor.quickquerybarts.s64559524)而非实际文本.
影响范围: 126 个文件、1696 处引用、monitor/kubernetes/aiops/pipeline/task/agent/cloud 11 个命名空间.

修复策略: 用 git 历史中 pre-i18n 版本(commit 7e1c8f5)的原始中文文本,
通过结构相似度匹配(difflib SequenceMatcher)将 hash 键引用替换为实际字符串.
对于组件拆分后产生的新文件,映射到其原始源文件查找中文文本.

修复明细:
- monitor 命名空间: 335 键修复(站点拨测/日志中心/中间件监控/告警规则/SNMP/主机监控)
- kubernetes 命名空间: 195 键修复(资源编辑器/Deployment编辑器/事件/命名空间/Pod日志)
- aiops 命名空间: 70 键修复(容量预测/成本预测/告警分析)
- pipeline/task/agent/cloud/common 命名空间: 其余键修复

验证: tsc -b=0, npm build✓ (16.87s), ESLint=0/0/0, go build=0, 17/17 测试通过, Docker healthy
2026-07-11 05:49:23 +08:00

225 lines
11 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { PageShell } from '@/components/layout/PageShell';
import { PageLoading } from '@/components/layout/PageLoading';
import { Monitor, HardDrive, Network, Server, Info, List, HeartPulse } from 'lucide-react';
import type { ZoomableChartHandle } from './host-monitor/ZoomableAreaChart';
import { DEFAULT_TIME_RANGE, PCT_REF_LINES, type AggType, type HostRecord, type HostMonitorTimeRange } from './host-monitor/types';
import { HostTimeRangePicker } from './host-monitor/HostTimeRangePicker';
import { getRangeSpanMinutes } from './host-monitor/timeRange';
import { parseJSON, buildMetricsFromHistory, getCurrentVal, filterHistoryByRange, getActualDataSpan } from './host-monitor/utils';
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 { BasicTab } from './host-monitor/tabs/BasicTab';
import { DiskTab } from './host-monitor/tabs/DiskTab';
import { NetworkTab } from './host-monitor/tabs/NetworkTab';
import { PortMonitorTab } from './host-monitor/tabs/PortMonitorTab';
import { ProcessTab } from './host-monitor/tabs/ProcessTab';
import type { JsonData } from '@/types/common';
import i18n from '@/i18n';
const TABS = [
{ id: 'basic', label: i18n.t('基础监控'), icon: <Monitor className="w-3.5 h-3.5" /> },
{ id: 'disk', label: i18n.t('磁盘监控'), icon: <HardDrive className="w-3.5 h-3.5" /> },
{ id: 'network', label: i18n.t('网络监控'), 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" /> },
];
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 as JsonData[] : (Array.isArray(parseJSON(latest?.port_info)) ? parseJSON(latest?.port_info) as unknown as JsonData[] : []);
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) as unknown as JsonData[] : [];
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 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('选择主机...')} />
</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('选择主机...')} </SelectItem>
<SelectItem value="max"> {i18n.t('选择主机...')} </SelectItem>
<SelectItem value="min"> {i18n.t('选择主机...')} </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">
{TABS.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">
<BasicTab metrics={metrics} latest={latest} currentVal={currentVal} aggType={aggType} pctRefLines={PCT_REF_LINES} />
</TabsContent>
<TabsContent value="disk" className="mt-3">
<DiskTab metrics={metrics} latest={latest} currentVal={currentVal} aggType={aggType} pctRefLines={PCT_REF_LINES} diskIOInfo={diskIOInfo} />
</TabsContent>
<TabsContent value="network" className="mt-3">
<NetworkTab metrics={metrics} netInfo={netInfo} currentVal={currentVal} aggType={aggType} />
</TabsContent>
<TabsContent value="ports" className="mt-3">
<PortMonitorTab portInfo={portInfo} portPolicyMode={portPolicyMode} portWhitelist={portWhitelist}
onOpenPortPolicy={() => { refetchPortPolicy(); setPortPolicyDialogOpen(true); }} />
</TabsContent>
<TabsContent value="process" className="mt-3">
<ProcessTab processTop={processTop} fdInfo={fdInfo} csInfo={csInfo} fdMetrics={metrics.fd}
aggType={aggType} fdChartRef={fdChartRef} fdChartZoomed={fdChartZoomed} setFdChartZoomed={setFdChartZoomed} />
</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('选择主机查看监控指标')}
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>
);
}