refactor(quality): Q27 前端巨型组件拆分 — HostMonitor 547L→224L (-59%), 提取 host-monitor/tabs/ 8 个文件 (BasicTab + DiskTab + NetworkTab + PortMonitorTab + ProcessTable + FDUsageCard + ContextSwitchCard + ProcessTab), TABS 常量数组提取为模块级, PCT_REF_LINES 常量提升到 types.ts, 端口/进程数组类型 JsonData→JsonData[] 类型安全升级, 零逻辑变更, ESLint errors=0/warnings=65/any=0, tsc -b=0, npm build✓ (17.64s), go build=0, 17/17 测试通过
This commit is contained in:
1 parent
03a84049fb
commit
470832342e
12 files changed
+563
-378
No files matched your search
@@ -129,7 +129,7 @@
|
||||
| ~~`api.ts` 1324 行/352 方法~~ → 已拆分为 12 个域文件 (api/kubernetes.ts, deploy.ts, monitor.ts 等) | ✅ 已解决 | 前端 | ~~P0~~ done |
|
||||
| 前端 **1026 处** `any` 类型 | 类型安全缺失,重构风险高 | 前端全局 | ~~P0~~ **done** (any 1076→40, unused 25→6, console 93→1, tsc --noEmit=0) |
|
||||
| ~~K8s 双路由体系(集群域+Kuboard 域)~~ → code 路由统一为主,ID 路由标记 deprecated 向后兼容 | ✅ 已解决 | 后端 | ~~P1~~ done |
|
||||
| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6-Q26 done: 最大组件 ResourceVisualEditor 882L→282L (-68%) + AgentCLI 814L→521L (-36%) + SystemSettingsTab 754L→173L (-77%) + NamespaceResourcePage 719L→300L (-58%) + ApprovalWorkflowPage 705L→167L (-76%) + PipelineRunDetail 678L→273L (-60%) + CloudServerImport 699L→404L (-42%,any[]→具体接口) + UnifiedQueryPanel 682L→529L (-22%) + ServerManager 652L→442L (-32%) + PipelineEditor 676L→406L (-40%) + RdpDialog 642L→442L (-31%) + DeploymentEditService 627L→394L (-37%) + TraceManager 605L→331L (-45%,修复 SpanTreeNode 内联定义反模式) + GrafanaDashboard 624L→78L (-87%,DRY 合并 Zabbix/Prometheus Tab + 移除文件级 eslint-disable + 类型安全升级) + **TopologyView 596L→472L (-21%,提取 TopologyCanvas/NodeDetailDialog/EditNodeDialog,移除文件级 eslint-disable + any→ClusterInfo 具体接口)** + **DeploymentEditAdvanced 590L→289L (-51%,提取 types(parser+builder 纯函数) + UpdateStrategyCard/TolerationsCard/PodMiscCard/PodSecurityCard/HostSettingsCard 5 个 Card 组件)** + **WorkloadViewPage 572L→348L (-39%,提取 types(parseWorkloadStats 纯函数) + shared-components(StatCard/LabelBadges/ContainerInfo) + UpdateStrategySelectorCards + PodInstancesTable + ScaleDialog)** + **CloudAccountManager 572L→308L + ExcelServerImport 560L→385L + PipelineCreateDialog 559L→498L** + **PodLogsViewer 553L→331L (-40%,提取 types(highlightKeyword 纯函数) + LogsHeader + ControlBar + LogContentArea + BottomStatusBar)**,累计提取 83 个独立组件文件** |
|
||||
| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6-Q27 done: 最大组件 ResourceVisualEditor 882L→282L (-68%) + AgentCLI 814L→521L (-36%) + SystemSettingsTab 754L→173L (-77%) + NamespaceResourcePage 719L→300L (-58%) + ApprovalWorkflowPage 705L→167L (-76%) + PipelineRunDetail 678L→273L (-60%) + CloudServerImport 699L→404L (-42%,any[]→具体接口) + UnifiedQueryPanel 682L→529L (-22%) + ServerManager 652L→442L (-32%) + PipelineEditor 676L→406L (-40%) + RdpDialog 642L→442L (-31%) + DeploymentEditService 627L→394L (-37%) + TraceManager 605L→331L (-45%,修复 SpanTreeNode 内联定义反模式) + GrafanaDashboard 624L→78L (-87%,DRY 合并 Zabbix/Prometheus Tab + 移除文件级 eslint-disable + 类型安全升级) + TopologyView 596L→472L (-21%) + DeploymentEditAdvanced 590L→289L (-51%) + WorkloadViewPage 572L→348L (-39%) + CloudAccountManager 572L→308L + ExcelServerImport 560L→385L + PipelineCreateDialog 559L→498L + PodLogsViewer 553L→331L (-40%) + **HostMonitor 547L→224L (-59%,提取 host-monitor/tabs/ 8 个文件 + TABS/PCT_REF_LINES 常量提取为模块级)**,累计提取 91 个独立组件文件** |
|
||||
|
||||
### 3.2 🟡 中优先级
|
||||
|
||||
|
||||
+41
@@ -498,6 +498,47 @@ services/api.ts (1324L)
|
||||
| Q24 | 前端巨型组件拆分(ExcelServerImport 560L→385L,提取 excel-import/types(6 个 CSV 纯函数) + StepIndicator + PreviewTable) | P0 | **done** |
|
||||
| Q25 | 前端巨型组件拆分(PipelineCreateDialog 559L→498L,提取 pipeline-create/types(PRESETS/DEFAULT_TARGET) + AdvancedSettingsPanel(172L 受控展开面板)) | P0 | **done** |
|
||||
| Q26 | 前端巨型组件拆分(PodLogsViewer 553L→331L,提取 pod-logs-viewer/types(highlightKeyword 纯函数) + LogsHeader + ControlBar + LogContentArea + BottomStatusBar) | P0 | **done** |
|
||||
| Q27 | 前端巨型组件拆分(HostMonitor 547L→224L (-59%),提取 host-monitor/tabs/ 8 个文件 (BasicTab + DiskTab + NetworkTab + PortMonitorTab + ProcessTable + FDUsageCard + ContextSwitchCard + ProcessTab),TABS 常量数组提取为模块级,PCT_REF_LINES 常量提升到 types.ts,端口/进程数组类型 JsonData→JsonData[] 类型安全升级) | P0 | **done** |
|
||||
|
||||
---
|
||||
|
||||
## Q27: 前端巨型组件拆分 — HostMonitor [done]
|
||||
|
||||
**问题**: `HostMonitor.tsx`(547 行)是 Q26 拆分后项目最大的前端业务组件之一,包含主机监控详情页面的全部逻辑:7 个 Tab(基础监控/磁盘/网络/端口/进程/系统信息/健康探针)、~15 个 useState/useRef 状态管理、react-query 数据加载、端口策略 CRUD、以及 5 个 Tab 的内联 JSX(基础监控 6 MetricChartCard / 磁盘 5 MetricChartCard / 网络 8 MetricChartCard+DualLineChart / 端口监听表格+策略配置 / 进程 TOP 表格+FD 趋势图+CTX/IRQ 卡片)。虽然部分子组件已提取(MetricChartCard/DualLineChart/HealthProbeSection/SystemInfoSection/HostCoreMetricsStrip),但 Tab 内容区仍全部内联。单文件过大导致维护困难。
|
||||
|
||||
**已完成 (2026-07-09)**:
|
||||
|
||||
将 5 个 Tab 内容区提取为 `host-monitor/tabs/` 子目录下的独立文件:
|
||||
|
||||
| 新文件 | 行数 | 职责 |
|
||||
|--------|------|------|
|
||||
| `BasicTab.tsx` | 35 | 基础监控 Tab(CPU/IO等待/内存/可用内存/Swap/系统负载 6 个 MetricChartCard),受控组件接收 metrics + latest + currentVal + aggType + pctRefLines |
|
||||
| `DiskTab.tsx` | 42 | 磁盘监控 Tab(磁盘使用/Inode/IO读写/IO繁忙度 5 个 MetricChartCard),受控组件接收 metrics + latest + currentVal + aggType + pctRefLines + diskIOInfo |
|
||||
| `NetworkTab.tsx` | 51 | 网络监控 Tab(TCP EST/TW/CW/总连接 4 MetricChartCard + DualLineChart 网卡带宽 + 接收/发送错误 2 MetricChartCard),受控组件接收 metrics + netInfo + currentVal + aggType |
|
||||
| `PortMonitorTab.tsx` | 73 | 端口监控 Tab(策略配置按钮 + 白名单/黑名单徽章 + 监听端口表格),受控组件接收 portInfo + portPolicyMode + portWhitelist + onOpenPortPolicy |
|
||||
| `ProcessTable.tsx` | 86 | 进程 TOP 表格(CMD/PID/内存%/CPU%/FDs 5 列 + 复制命令按钮 + clipboard API try/catch),纯展示组件接收 processTop |
|
||||
| `FDUsageCard.tsx` | 79 | FD 文件句柄卡片(已分配/Max/使用率 3 项指标 + ZoomableAreaChart 趋势图 + ChartToolbar 缩放控制),受控组件接收 fdInfo + fdMetrics + aggType + fdChartRef + fdChartZoomed + setFdChartZoomed |
|
||||
| `ContextSwitchCard.tsx` | 55 | 上下文切换/中断卡片(CTX/IRQ 大数字卡片 + CTX/IRQ 比例条),纯展示组件接收 csInfo |
|
||||
| `ProcessTab.tsx` | 39 | 进程监控 Tab 编排器(ProcessTable + FDUsageCard + ContextSwitchCard 两列布局),接收全部进程 Tab props 并分发 |
|
||||
|
||||
主文件 `HostMonitor.tsx` 从 **547 行降至 224 行**(-59%),仅保留:状态管理(selectedHost/activeTab/timeRange/aggType/portPolicy 4 state + fdChartRef/fdChartZoomed 2 ref)、react-query 数据加载(useHostList/useHostHistory/usePortPolicy)、useEffect(portPolicy 同步 + hosts 自动选择)、handleSavePortPolicy 回调、useMemo 派生数据(history/spaMinutes/actualDataSpan/latest/metrics)、JSON 解析(netInfo/portInfo/diskIOInfo/swapInfo/processTop/fdInfo/csInfo/heartbeatInfo)、currentVal 闭包函数、toolbar JSX、TABS 常量数组、monitorBody Tabs 骨架(7 TabsContent 分发到子组件)、dialogs JSX、embedded/full 渲染分支。
|
||||
|
||||
**关键改进**:
|
||||
- **TABS 常量提取**: 原 7 个 Tab 的 id/label/icon 定义内联在 `monitorBody` JSX 的 `.map()` 数组中(每次 render 重建),提取为模块级 `TABS` 常量数组,消除重复创建。
|
||||
- **PCT_REF_LINES 常量提升**: 原 `pctRefLines` 在组件函数体内定义(每次 render 重建),提升到 `types.ts` 作为模块级导出常量 `PCT_REF_LINES`,同时定义 `RefLine` 接口供 Tab 组件类型约束。
|
||||
- **类型安全升级**: `portInfo` 和 `processTop` 从 `JsonData`(单个对象)改为 `JsonData[]`(对象数组),并加 `as unknown as JsonData[]` 类型断言解决 `parseJSON` 返回类型与数组类型不兼容的 TS2345 错误。
|
||||
|
||||
**验收标准**:
|
||||
- [x] HostMonitor.tsx 从 547 行降至 **224 行**(-59%)
|
||||
- [x] 8 个提取的文件全部 < 90 行(最大 ProcessTable 86 行,ProcessTab 编排器仅 39 行)
|
||||
- [x] `tsc -b` 零错误
|
||||
- [x] `npm run build` 零错误(✓ built in 17.64s)
|
||||
- [x] ESLint errors 保持 **0**(HostMonitor + host-monitor/tabs/ 目录零 errors/warnings)
|
||||
- [x] ESLint warnings 保持 **65**(无新增)
|
||||
- [x] 全局 ESLint any 保持 **0**
|
||||
- [x] `go build` 零错误
|
||||
- [x] 17/17 集成回归测试通过(零回归)
|
||||
- [x] 主机监控详情页功能不变(7 个 Tab 全部正常:基础监控/磁盘/网络/端口/进程/系统信息/健康探针)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -6,33 +6,38 @@ 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 { 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, formatBPS, buildMetricsFromHistory, getCurrentVal, filterHistoryByRange, getActualDataSpan } from './host-monitor/utils';
|
||||
import { MetricChartCard } from './host-monitor/MetricChartCard';
|
||||
import { DualLineChart } from './host-monitor/DualLineChart';
|
||||
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('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" /> },
|
||||
];
|
||||
|
||||
export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
const qc = useQueryClient();
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -82,19 +87,10 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
} finally { setPortPolicySaving(false); }
|
||||
};
|
||||
|
||||
const history = useMemo(
|
||||
() => filterHistoryByRange(rawHistory, timeRange),
|
||||
[rawHistory, timeRange],
|
||||
);
|
||||
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 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 => {
|
||||
@@ -106,10 +102,10 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
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 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) : [];
|
||||
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);
|
||||
@@ -117,11 +113,6 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
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">
|
||||
@@ -143,9 +134,7 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
</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>
|
||||
<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>
|
||||
@@ -165,15 +154,7 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
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 => (
|
||||
{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}
|
||||
@@ -181,334 +162,40 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
))}
|
||||
</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="basic" className="mt-3">
|
||||
<BasicTab metrics={metrics} latest={latest} currentVal={currentVal} aggType={aggType} pctRefLines={PCT_REF_LINES} />
|
||||
</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="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 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="network" className="mt-3">
|
||||
<NetworkTab metrics={metrics} netInfo={netInfo} currentVal={currentVal} aggType={aggType} />
|
||||
</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="ports" className="mt-3">
|
||||
<PortMonitorTab portInfo={portInfo} portPolicyMode={portPolicyMode} portWhitelist={portWhitelist}
|
||||
onOpenPortPolicy={() => { refetchPortPolicy(); setPortPolicyDialogOpen(true); }} />
|
||||
</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>
|
||||
<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>
|
||||
|
||||
<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}
|
||||
/>
|
||||
<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}
|
||||
/>
|
||||
<PortPolicyDialog open={portPolicyDialogOpen} onOpenChange={setPortPolicyDialogOpen}
|
||||
portPolicyMode={portPolicyMode} onModeChange={setPortPolicyMode}
|
||||
portWhitelist={portWhitelist} onWhitelistChange={setPortWhitelist}
|
||||
onSave={handleSavePortPolicy} saving={portPolicySaving} />
|
||||
);
|
||||
|
||||
if (embedded) {
|
||||
@@ -516,12 +203,7 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
<div className="space-y-3">
|
||||
{toolbar}
|
||||
{!loading && history.length > 0 && (
|
||||
<HostCoreMetricsStrip
|
||||
latest={latest}
|
||||
netInfo={netInfo}
|
||||
diskIOInfo={diskIOInfo}
|
||||
onJump={setActiveTab}
|
||||
/>
|
||||
<HostCoreMetricsStrip latest={latest} netInfo={netInfo} diskIOInfo={diskIOInfo} onJump={setActiveTab} />
|
||||
)}
|
||||
{loading && !history.length ? <PageLoading /> : monitorBody}
|
||||
{dialogs}
|
||||
@@ -530,16 +212,11 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<PageShell
|
||||
variant="workspace"
|
||||
title={i18n.t('monitor.alertRulesTab.catHost')}
|
||||
<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"
|
||||
>
|
||||
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>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// 主机监控 — 基础监控 Tab (CPU/IO等待/内存/可用内存/Swap/负载)
|
||||
import { Cpu, Clock, Server, HardDrive, Activity } from 'lucide-react';
|
||||
import type { AggType, BucketPoint, HostRecord, RefLine } from '../types';
|
||||
import { MetricChartCard } from '../MetricChartCard';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
export interface BasicTabProps {
|
||||
metrics: {
|
||||
cpu: BucketPoint[]; iowait: BucketPoint[]; mem: BucketPoint[];
|
||||
memAvail: BucketPoint[]; swap: BucketPoint[]; load1: BucketPoint[];
|
||||
};
|
||||
latest: HostRecord;
|
||||
currentVal: (key: string, fallback?: string) => string;
|
||||
aggType: AggType;
|
||||
pctRefLines: RefLine[];
|
||||
}
|
||||
|
||||
export function BasicTab({ metrics, latest, currentVal, aggType, pctRefLines }: BasicTabProps) {
|
||||
return (
|
||||
<div className="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} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// 主机监控 — 上下文切换/中断卡片 (CTX/IRQ 大数字 + 比例条)
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
export function ContextSwitchCard({ csInfo }: { csInfo: JsonData }) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// 主机监控 — 磁盘监控 Tab (磁盘使用/Inode/IO读写/IO繁忙度)
|
||||
import { HardDrive, Disc } from 'lucide-react';
|
||||
import type { AggType, BucketPoint, RefLine } from '../types';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { MetricChartCard } from '../MetricChartCard';
|
||||
import { parseJSON } from '../utils';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
export interface DiskTabProps {
|
||||
metrics: {
|
||||
disk: BucketPoint[]; inode: BucketPoint[]; ioRead: BucketPoint[];
|
||||
ioWrite: BucketPoint[]; ioUtil: BucketPoint[];
|
||||
};
|
||||
latest: JsonData;
|
||||
currentVal: (key: string, fallback?: string) => string;
|
||||
aggType: AggType;
|
||||
pctRefLines: RefLine[];
|
||||
diskIOInfo: JsonData;
|
||||
}
|
||||
|
||||
export function DiskTab({ metrics, latest, currentVal, aggType, pctRefLines, diskIOInfo }: DiskTabProps) {
|
||||
const inodeVal = (() => {
|
||||
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)}%` : '--';
|
||||
})();
|
||||
|
||||
return (
|
||||
<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={inodeVal} 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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// 主机监控 — FD 文件句柄卡片 (已分配/Max/使用率 + 趋势图)
|
||||
import type { RefObject } from 'react';
|
||||
import { HardDrive } from 'lucide-react';
|
||||
import type { AggType, BucketPoint } from '../types';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { MetricIconBox } from '../MetricIconBox';
|
||||
import { ChartToolbar } from '../ChartToolbar';
|
||||
import { ZoomableAreaChart, type ZoomableChartHandle } from '../ZoomableAreaChart';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
export interface FDUsageCardProps {
|
||||
fdInfo: JsonData;
|
||||
fdMetrics: BucketPoint[];
|
||||
aggType: AggType;
|
||||
fdChartRef: RefObject<ZoomableChartHandle | null>;
|
||||
fdChartZoomed: boolean;
|
||||
setFdChartZoomed: (v: boolean) => void;
|
||||
}
|
||||
|
||||
export function FDUsageCard({ fdInfo, fdMetrics, aggType, fdChartRef, fdChartZoomed, setFdChartZoomed }: FDUsageCardProps) {
|
||||
return (
|
||||
<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>
|
||||
{fdMetrics.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>
|
||||
{fdMetrics.length > 0 && (
|
||||
<ZoomableAreaChart
|
||||
ref={fdChartRef}
|
||||
data={fdMetrics.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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// 主机监控 — 网络监控 Tab (TCP连接/网卡带宽/网卡错误)
|
||||
import { Network, Clock, XCircle } from 'lucide-react';
|
||||
import type { AggType, BucketPoint } from '../types';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { MetricChartCard } from '../MetricChartCard';
|
||||
import { DualLineChart } from '../DualLineChart';
|
||||
import { formatBPS } from '../utils';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
export interface NetworkTabProps {
|
||||
metrics: {
|
||||
tcpEst: BucketPoint[]; tcpTW: BucketPoint[]; tcpCW: BucketPoint[];
|
||||
connTotal: BucketPoint[]; rx: BucketPoint[]; tx: BucketPoint[];
|
||||
rxErr: BucketPoint[]; txErr: BucketPoint[];
|
||||
};
|
||||
netInfo: JsonData;
|
||||
currentVal: (key: string, fallback?: string) => string;
|
||||
aggType: AggType;
|
||||
}
|
||||
|
||||
export function NetworkTab({ metrics, netInfo, currentVal, aggType }: NetworkTabProps) {
|
||||
const connTotal = netInfo ? String((netInfo.tcp_established || 0) + (netInfo.tcp_time_wait || 0) + (netInfo.tcp_close_wait || 0)) : '--';
|
||||
|
||||
return (
|
||||
<div className="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={connTotal} 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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// 主机监控 — 端口监控 Tab (监听端口表 + 策略配置)
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Server } from 'lucide-react';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
export interface PortMonitorTabProps {
|
||||
portInfo: JsonData[];
|
||||
portPolicyMode: string;
|
||||
portWhitelist: number[];
|
||||
onOpenPortPolicy: () => void;
|
||||
}
|
||||
|
||||
export function PortMonitorTab({ portInfo, portPolicyMode, portWhitelist, onOpenPortPolicy }: PortMonitorTabProps) {
|
||||
return (
|
||||
<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={onOpenPortPolicy}>
|
||||
策略配置
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// 主机监控 — 进程监控 Tab (进程TOP表 + FD卡片 + CTX/IRQ卡片)
|
||||
import type { RefObject } from 'react';
|
||||
import type { AggType, BucketPoint } from '../types';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { ProcessTable } from './ProcessTable';
|
||||
import { FDUsageCard } from './FDUsageCard';
|
||||
import { ContextSwitchCard } from './ContextSwitchCard';
|
||||
import type { ZoomableChartHandle } from '../ZoomableAreaChart';
|
||||
|
||||
export interface ProcessTabProps {
|
||||
processTop: JsonData[];
|
||||
fdInfo: JsonData;
|
||||
csInfo: JsonData;
|
||||
fdMetrics: BucketPoint[];
|
||||
aggType: AggType;
|
||||
fdChartRef: RefObject<ZoomableChartHandle | null>;
|
||||
fdChartZoomed: boolean;
|
||||
setFdChartZoomed: (v: boolean) => void;
|
||||
}
|
||||
|
||||
export function ProcessTab(props: ProcessTabProps) {
|
||||
const { processTop, fdInfo, csInfo, fdMetrics, aggType, fdChartRef, fdChartZoomed, setFdChartZoomed } = props;
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<ProcessTable processTop={processTop} />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<FDUsageCard
|
||||
fdInfo={fdInfo}
|
||||
fdMetrics={fdMetrics}
|
||||
aggType={aggType}
|
||||
fdChartRef={fdChartRef}
|
||||
fdChartZoomed={fdChartZoomed}
|
||||
setFdChartZoomed={setFdChartZoomed}
|
||||
/>
|
||||
<ContextSwitchCard csInfo={csInfo} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// 主机监控 — 进程 TOP 表格
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { List } from 'lucide-react';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
export function ProcessTable({ processTop }: { processTop: JsonData[] }) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -70,6 +70,13 @@ export interface ProbeForm {
|
||||
description: string;
|
||||
}
|
||||
|
||||
/** 百分比参考线常量(80%/90%)用于 CPU/内存/磁盘等图表 */
|
||||
export interface RefLine { value: number; label: string; color: string; }
|
||||
export const PCT_REF_LINES: RefLine[] = [
|
||||
{ value: 80, label: '80%', color: 'var(--dg-yellow)' },
|
||||
{ value: 90, label: '90%', color: 'var(--dg-red)' },
|
||||
];
|
||||
|
||||
export const DEFAULT_PROBE_FORM = (hostname = ''): ProbeForm => ({
|
||||
name: '',
|
||||
hostname,
|
||||
|
||||
Reference in new issue
Block a user