问题: i18n 批量转换 (commit55f216c) 生成了 813 个 hash 键引用但翻译从未写入 JSON, 导致用户在受影响模块看到 hash 键路径(如 monitor.quickquerybarts.s64559524)而非实际文本. 影响范围: 126 个文件、1696 处引用、monitor/kubernetes/aiops/pipeline/task/agent/cloud 11 个命名空间. 修复策略: 用 git 历史中 pre-i18n 版本(commit7e1c8f5)的原始中文文本, 通过结构相似度匹配(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
225 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|