refactor(frontend): api/components/pages 目录架构统一对齐菜单域 + 日志中心修复
1. 目录架构统一(API = components = 菜单域,一域一目录 + barrel) - log/ 提为独立顶层域(日志中心在菜单中与监控中心平级,不再塞在 monitor/) - task/ 拆分原 automation.ts → cron-job / script / remote-exec - aiops/ 合并 ai.ts + agentCli.ts → assistant + agent-cli - agent/ approval/ audit/ 提升为域目录 - auth 拆分:authApi(认证自服务) + system/user.ts(userApi+ldapApi 管理端) - components/automation → components/task,菜单"自动化运维"→"作业任务" - pages/ 按域分子目录(approval/audit/monitor),删孤儿 AuditPage.tsx - 所有 API 符号名不变,组件经 @/api 聚合引用,运行时行为零变化 2. Loki 查询无返回修复:aiops 容器 DNS 解析外网域名失败 (宿主机 systemd-resolved 转发链路异常),docker-compose 显式配 dns 上游 3. ES 查询框布局:工具栏单行挤 7 控件(ES 多 150px 索引框),拆两行—— 查询输入独占一行(+查询按钮),次级控件换行 flex-wrap Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
da01cbb119
commit
c0ee8e4ddd
74 files changed
+229
-216
No files matched your search
@@ -0,0 +1,481 @@
|
||||
// 主机监控详情 — 阿里云云监控风格 + v2.0 新增指标
|
||||
// 进程TOP / Swap / FD / 上下文切换 // Logan.Li
|
||||
import { useState, useEffect, useMemo } 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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Cpu, HardDrive, Network, Activity, Server, Loader2,
|
||||
Info, Monitor, Disc, Clock, List, HeartPulse, RefreshCw, XCircle,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
import { TIME_RANGES, type AggType, type HostRecord } from './host-monitor/types';
|
||||
import { parseJSON, formatBPS, buildMetricsFromHistory, getCurrentVal } 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 type { JsonData } from '@/types/common';
|
||||
|
||||
export function HostMonitor() {
|
||||
const qc = useQueryClient();
|
||||
const [searchParams] = useSearchParams();
|
||||
const urlHost = searchParams.get('host') || '';
|
||||
const [selectedHost, setSelectedHost] = useState(urlHost);
|
||||
const [activeTab, setActiveTab] = useState('basic');
|
||||
const [rangeHours, setRangeHours] = useState(1);
|
||||
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 { data: hosts = [], isLoading: hostsLoading } = useHostList();
|
||||
const { data: history = [], isLoading: historyLoading } = useHostHistory(selectedHost, rangeHours);
|
||||
const { data: portPolicy, refetch: refetchPortPolicy } = usePortPolicy(selectedHost);
|
||||
|
||||
useEffect(() => {
|
||||
if (portPolicy) {
|
||||
setPortPolicyMode(portPolicy.port_filter_mode);
|
||||
setPortWhitelist(portPolicy.port_whitelist);
|
||||
}
|
||||
}, [portPolicy]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hosts.length > 0 && !selectedHost) {
|
||||
const m = urlHost ? hosts.find((h) => h.hostname === urlHost) : null;
|
||||
setSelectedHost(String(m?.hostname || hosts[0]?.hostname || ''));
|
||||
}
|
||||
}, [hosts, selectedHost, urlHost]);
|
||||
|
||||
const handleSavePortPolicy = async () => {
|
||||
if (!selectedHost) return;
|
||||
setPortPolicySaving(true);
|
||||
try {
|
||||
await agentApi.savePortPolicy(selectedHost, {
|
||||
port_filter_mode: portPolicyMode,
|
||||
port_whitelist: portWhitelist,
|
||||
});
|
||||
setPortPolicyDialogOpen(false);
|
||||
qc.invalidateQueries({ queryKey: queryKeys.hostMonitor.portPolicy(selectedHost) });
|
||||
} catch { } finally { setPortPolicySaving(false); }
|
||||
};
|
||||
|
||||
const timeConfig = TIME_RANGES.find(tr => tr.hours === rangeHours) || TIME_RANGES[0];
|
||||
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);
|
||||
|
||||
if (loading && !history.length) {
|
||||
return <div className="flex items-center justify-center h-64"><Loader2 className="h-8 w-8 animate-spin text-slate-500" /></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Server className="h-4 w-4 text-blue-400" />
|
||||
<Select value={selectedHost} onValueChange={setSelectedHost}>
|
||||
<SelectTrigger className="w-52 h-8 text-xs bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<SelectValue placeholder="选择主机..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700 text-slate-200">
|
||||
{hosts.map((h: JsonData) => (
|
||||
<SelectItem key={h.hostname} value={h.hostname}>{h.hostname}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{serverInfo && (
|
||||
<span className="text-xs text-[var(--dg-text-muted)] ml-2">
|
||||
{String(serverInfo.os || '')}{serverInfo.cpu_cores ? ` | ${serverInfo.cpu_cores as number | string} 核` : ''}{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 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700 text-slate-200">
|
||||
<SelectItem value="avg">平均值</SelectItem>
|
||||
<SelectItem value="max">最大值</SelectItem>
|
||||
<SelectItem value="min">最小值</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex items-center gap-1">
|
||||
{TIME_RANGES.map(tr => (
|
||||
<Button key={tr.hours} variant={rangeHours === tr.hours ? 'default' : 'outline'} size="sm"
|
||||
className="h-7 text-xs px-2.5" onClick={() => setRangeHours(tr.hours)}>{tr.label}</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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: '基础监控', icon: <Monitor className="w-3 h-3" /> },
|
||||
{ id: 'disk', label: '磁盘监控', icon: <HardDrive className="w-3 h-3" /> },
|
||||
{ id: 'network', label: '网络监控', icon: <Network className="w-3 h-3" /> },
|
||||
{ id: 'ports', label: '端口监控', icon: <Server className="w-3 h-3" /> },
|
||||
{ id: 'process', label: '进程监控', icon: <List className="w-3 h-3" /> },
|
||||
{ id: 'system', label: '系统信息', icon: <Info className="w-3 h-3" /> },
|
||||
{ id: 'health_probes', label: '健康探针', icon: <HeartPulse className="w-3 h-3" /> },
|
||||
].map(tab => (
|
||||
<TabsTrigger key={tab.id} value={tab.id} className="text-[11px] h-7 gap-1 data-[state=active]:bg-[var(--dg-bg-primary)]">
|
||||
{tab.icon}{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="basic" className="mt-3 grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<MetricChartCard title="CPU 使用率(%)" value={currentVal('cpu')} icon={<Cpu className="h-4 w-4 text-blue-400" />}
|
||||
data={metrics.cpu} color="#3b82f6" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||
<MetricChartCard title="CPU IO等待(%)" value={latest?.cpu_iowait != null ? `${(latest.cpu_iowait as number).toFixed(1)}%` : '--'} icon={<Clock className="h-4 w-4 text-orange-400" />}
|
||||
data={metrics.iowait} color="#f97316" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||
<MetricChartCard title="内存使用率(%)" value={currentVal('mem')} icon={<Server className="h-4 w-4 text-emerald-400" />}
|
||||
data={metrics.mem} color="#10b981" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||
<MetricChartCard title="可用内存(GB)" value={latest?.mem_available != null ? `${(latest.mem_available as number).toFixed(1)}GB` : '--'} icon={<Server className="h-4 w-4 text-emerald-400" />}
|
||||
data={metrics.memAvail} color="#34d399" unit="GB" domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title="Swap 使用率(%)" value={currentVal('swap')} icon={<HardDrive className="h-4 w-4 text-red-400" />}
|
||||
data={metrics.swap} color="#ef4444" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||
<MetricChartCard title="系统负载(1m)" value={currentVal('load1')} icon={<Activity className="h-4 w-4 text-purple-400" />}
|
||||
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="磁盘使用率(%)" value={currentVal('disk')} icon={<HardDrive className="h-4 w-4 text-orange-400" />}
|
||||
data={metrics.disk} color="#f97316" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||
<MetricChartCard title="Inode 使用率(%)" 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 className="h-4 w-4 text-purple-400" />}
|
||||
data={metrics.inode} color="#8b5cf6" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||
<MetricChartCard title="磁盘读取(KB/s)" value={currentVal('ioR')} icon={<Disc className="h-4 w-4 text-blue-400" />}
|
||||
data={metrics.ioRead} color="#3b82f6" unit="KB/s" domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title="磁盘写入(KB/s)" value={currentVal('ioW')} icon={<Disc className="h-4 w-4 text-emerald-400" />}
|
||||
data={metrics.ioWrite} color="#10b981" unit="KB/s" domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title="磁盘IO繁忙度(%)" value={diskIOInfo?.util != null ? `${diskIOInfo.util.toFixed(1)}%` : '--'} icon={<Disc className="h-4 w-4 text-red-400" />}
|
||||
data={metrics.ioUtil} color="#ef4444" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||
</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 className="h-4 w-4 text-purple-400" />}
|
||||
data={metrics.tcpEst} color="#8b5cf6" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title="TCP TIME_WAIT" value={currentVal('tw')} icon={<Clock className="h-4 w-4 text-amber-400" />}
|
||||
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 className="h-4 w-4 text-red-400" />}
|
||||
data={metrics.tcpCW} color="#ef4444" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title="TCP 总连接" value={netInfo ? String((netInfo.tcp_established||0)+(netInfo.tcp_time_wait||0)+(netInfo.tcp_close_wait||0)) : '--'} icon={<Network className="h-4 w-4 text-cyan-400" />}
|
||||
data={metrics.connTotal} color="#22d3ee" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||
</div>
|
||||
<DualLineChart
|
||||
title="网卡带宽吞吐"
|
||||
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="网卡接收错误" value={netInfo?.net_rx_errors != null ? String(netInfo.net_rx_errors) : '--'} icon={<XCircle className="h-4 w-4 text-red-400" />}
|
||||
data={metrics.rxErr} color="#ef4444" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title="网卡发送错误" value={netInfo?.net_tx_errors != null ? String(netInfo.net_tx_errors) : '--'} icon={<XCircle className="h-4 w-4 text-orange-400" />}
|
||||
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 text-emerald-400" />监听端口 {portInfo.length}
|
||||
<span className="ml-auto flex items-center gap-2">
|
||||
{portPolicyMode === 'blacklist' && portWhitelist.length > 0 && (
|
||||
<Badge variant="outline" className="text-[10px] text-amber-400 border-amber-500/30">
|
||||
黑名单: {portWhitelist.join(',')}
|
||||
</Badge>
|
||||
)}
|
||||
{portPolicyMode === 'whitelist' && (
|
||||
<Badge variant="outline" className="text-[10px] text-blue-400 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">端口</th>
|
||||
<th className="py-1.5 text-left font-medium">协议</th>
|
||||
<th className="py-1.5 text-left font-medium">进程</th>
|
||||
<th className="py-1.5 text-right font-medium">状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{portInfo.map((p: JsonData, i: number) => (
|
||||
<tr key={i} className="border-b border-[var(--dg-border)]/30 hover:bg-slate-800/30 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 text-emerald-400 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 text-cyan-400" />进程 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%]">进程名称 / CMD</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%]">内存使用率(%)</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%]">打开文件数</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-slate-800/30 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:text-cyan-400 shrink-0"
|
||||
title="复制命令行"
|
||||
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' : 'text-cyan-400'}>
|
||||
{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">
|
||||
<h3 className="text-xs font-medium text-[var(--dg-text-primary)] mb-3 flex items-center gap-2">
|
||||
<HardDrive className="h-4 w-4 text-violet-400" />FD 文件句柄
|
||||
</h3>
|
||||
{fdInfo?.allocated != null ? (
|
||||
<>
|
||||
<div className="flex items-center gap-4 mb-3 text-xs">
|
||||
<div>
|
||||
<span className="text-[var(--dg-text-muted)]">已分配 </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)]">上限 </span>
|
||||
<span className="font-mono text-[var(--dg-text-primary)] font-semibold">{fdInfo.max === -1 ? '无限制' : fdInfo.max?.toLocaleString() || '—'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[var(--dg-text-muted)]">使用率 </span>
|
||||
<span className={`font-mono font-semibold ${fdInfo.usage_pct > 80 ? 'text-rose-400' : 'text-violet-400'}`}>
|
||||
{fdInfo.usage_pct != null ? `${fdInfo.usage_pct.toFixed(1)}%` : '—'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{metrics.fd.length > 0 && (
|
||||
<div className="h-[120px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={metrics.fd}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--dg-border)" />
|
||||
<XAxis dataKey="time" tick={{ fontSize: 10, fill: 'var(--dg-text-muted)' }} interval="preserveStartEnd" />
|
||||
<YAxis tick={{ fontSize: 10, fill: 'var(--dg-text-muted)' }} unit="%" domain={[0, 'auto']} />
|
||||
<Tooltip contentStyle={{ background: '#1e293b', border: '1px solid #334155', borderRadius: 6, fontSize: 11 }} />
|
||||
<defs>
|
||||
<linearGradient id="fdGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#a78bfa" stopOpacity={0.3} />
|
||||
<stop offset="100%" stopColor="#a78bfa" stopOpacity={0.05} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area type="monotone" dataKey={aggType} stroke="#a78bfa" strokeWidth={1.5} fill="url(#fdGrad)" name="FD使用率" />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-4 text-xs text-[var(--dg-text-muted)]">等待数据…</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 text-emerald-400" />上下文切换
|
||||
</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 text-emerald-400">
|
||||
{(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 text-amber-400">
|
||||
{(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="text-emerald-400">CTX</span>
|
||||
<span className="text-amber-400">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)]">等待数据…</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<SystemInfoSection
|
||||
selectedHost={selectedHost}
|
||||
serverInfo={serverInfo}
|
||||
latest={latest}
|
||||
heartbeatInfo={heartbeatInfo}
|
||||
portInfoCount={portInfo.length}
|
||||
/>
|
||||
|
||||
<HealthProbeSection selectedHost={selectedHost} />
|
||||
</Tabs>
|
||||
|
||||
<PortPolicyDialog
|
||||
open={portPolicyDialogOpen}
|
||||
onOpenChange={setPortPolicyDialogOpen}
|
||||
portPolicyMode={portPolicyMode}
|
||||
onModeChange={setPortPolicyMode}
|
||||
portWhitelist={portWhitelist}
|
||||
onWhitelistChange={setPortWhitelist}
|
||||
onSave={handleSavePortPolicy}
|
||||
saving={portPolicySaving}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user