fix: HostMonitor 下拉主机名修复 - 补回 setHosts(list)

This commit is contained in:
attacker committed 2026-06-24 16:46:49 +00:00
1 parent 8240a88a84
commit f061785266
1 file changed
+139 -43
+139 -43
View File
@@ -103,7 +103,7 @@ export function HostMonitor() {
}, [selectedHost]);
// 端口策略 // Logan.Li
const [portPolicyMode, setPortPolicyMode] = useState('blacklist');
const [portPolicyMode, setPortPolicyMode] = useState('whitelist');
const [portWhitelist, setPortWhitelist] = useState<number[]>([]);
const [portPolicyDialogOpen, setPortPolicyDialogOpen] = useState(false);
const [portPolicySaving, setPortPolicySaving] = useState(false);
@@ -141,6 +141,7 @@ export function HostMonitor() {
useEffect(() => {
agentApi.getLatest().then((data: any) => {
const list = Array.isArray(data) ? data : (data?.data || data?.list || []);
setHosts(list);
if (list.length > 0 && !selectedHost) {
const m = urlHost ? list.find((h: any) => h.hostname === urlHost) : null;
setSelectedHost(m?.hostname || list[0]?.hostname || '');
@@ -169,7 +170,7 @@ export function HostMonitor() {
// 历史指标提取
const metrics = useMemo(() => {
const rawCpu: number[] = []; const rawMem: number[] = []; const rawLoad1: number[] = [];
const rawDisk: number[] = []; const rawTcp: number[] = []; const rawTw: number[] = [];
const rawDisk: number[] = []; const rawTcp: number[] = []; const rawTw: number[] = []; const rawCw: number[] = [];
const rawRx: number[] = []; const rawTx: number[] = [];
const rawIoR: number[] = []; const rawIoW: number[] = [];
const rawSwap: number[] = []; const rawFD: number[] = [];
@@ -177,6 +178,7 @@ export function HostMonitor() {
const rawIOWait: number[] = []; const rawMemAvail: number[] = [];
const rawIoUtil: number[] = []; const rawConnTotal: number[] = [];
const rawRxErr: number[] = []; const rawTxErr: number[] = [];
const rawInode: number[] = []; // 根分区 inode
const timestamps: number[] = [];
for (const r of history) {
@@ -189,9 +191,18 @@ export function HostMonitor() {
rawLoad1.push(r.load1 || 0);
rawDisk.push(r.disk_usage || 0);
const diskInfoArr = parseJSON(r.disk_info);
if (Array.isArray(diskInfoArr) && diskInfoArr.length > 0) {
const rootDisk = diskInfoArr.find((d: any) => d.mount === '/') || diskInfoArr[0];
rawInode.push(rootDisk?.inode_usage_pct || 0);
} else {
rawInode.push(0);
}
const net = parseJSON(r.net_info);
rawTcp.push(net.tcp_established || 0);
rawTw.push(net.tcp_time_wait || 0);
rawCw.push(net.tcp_close_wait || 0);
rawConnTotal.push((net.tcp_established || 0) + (net.tcp_time_wait || 0) + (net.tcp_close_wait || 0));
rawRx.push(net.net_rx_bps || 0);
rawTx.push(net.net_tx_bps || 0);
@@ -217,6 +228,7 @@ export function HostMonitor() {
disk: bucketWithStats(rawDisk, timestamps, timeConfig.buckets),
tcpEst: bucketWithStats(rawTcp, timestamps, timeConfig.buckets),
tcpTW: bucketWithStats(rawTw, timestamps, timeConfig.buckets),
tcpCW: bucketWithStats(rawCw, timestamps, timeConfig.buckets),
rx: bucketWithStats(rawRx.map(v => v / 1024), timestamps, timeConfig.buckets),
tx: bucketWithStats(rawTx.map(v => v / 1024), timestamps, timeConfig.buckets),
ioRead: bucketWithStats(rawIoR.map(v => v / 1024), timestamps, timeConfig.buckets),
@@ -230,6 +242,7 @@ export function HostMonitor() {
connTotal: bucketWithStats(rawConnTotal, timestamps, timeConfig.buckets),
rxErr: bucketWithStats(rawRxErr, timestamps, timeConfig.buckets),
txErr: bucketWithStats(rawTxErr, timestamps, timeConfig.buckets),
inode: bucketWithStats(rawInode, timestamps, timeConfig.buckets),
};
}, [history, timeConfig]);
@@ -281,6 +294,7 @@ export function HostMonitor() {
</Select>
{serverInfo && (
<span className="text-xs text-[var(--dg-text-muted)] ml-2">
<span className="text-sm font-semibold text-[var(--dg-text-primary)] mr-2">{selectedHost}</span>
{serverInfo.os || ''} | {serverInfo.cpu_cores || '?'} 核 | {serverInfo.mem_total ? `${serverInfo.mem_total.toFixed(0)}GB` : '?'}
</span>
)}
@@ -344,6 +358,12 @@ export function HostMonitor() {
<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: any) => 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" />}
@@ -353,14 +373,16 @@ export function HostMonitor() {
</div>
</TabsContent>
{/* 网络监控 — 双线网卡带宽 + TIME_WAIT + TCP连接 */}
{/* 网络监控 — TCP连接 + 带宽 + 错误 */}
<TabsContent value="network" className="mt-3 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<MetricChartCard title="TCP 连接数(EST)" value={currentVal('tcp')} icon={<Network className="h-4 w-4 text-purple-400" />}
<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 总连接数" 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" />}
<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>
{/* 网卡带宽吞吐 — 双线 rx/tx */}
@@ -442,8 +464,9 @@ export function HostMonitor() {
</Card>
</TabsContent>
{/* ═══ 进程监控 — 简洁 5 列 ═══ */}
{/* ═══ 进程监控 — 进程 TOP + FD 趋势 + 上下文切换 ═══ */}
<TabsContent value="process" className="mt-3 space-y-3">
{/* ── 进程 TOP 表格 ── */}
<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">
@@ -524,6 +547,107 @@ export function HostMonitor() {
)}
</CardContent>
</Card>
{/* ── FD 文件句柄 + 上下文切换 可视化 ── */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{/* FD 卡片 + 趋势图 */}
<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>
{/* 系统信息 */}
@@ -571,39 +695,6 @@ export function HostMonitor() {
</Card>
)}
{/* FD 文件句柄 + 上下文切换 */}
{(fdInfo || csInfo) && (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-2">
<HardDrive className="h-4 w-4 text-[var(--dg-text-muted)]" />
<h3 className="text-xs font-medium text-[var(--dg-text-primary)]">FD 文件句柄 &amp; 上下文切换</h3>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-3 text-xs">
{fdInfo?.allocated != null && [
['FD 已分配', <span className="font-mono">{fdInfo.allocated.toLocaleString()}</span>],
['FD 上限', <span className="font-mono">{fdInfo.max === -1 ? '无限制' : fdInfo.max?.toLocaleString() || '—'}</span>],
['FD 使用率', <span className={`font-mono ${fdInfo.usage_pct > 80 ? 'text-rose-400' : 'text-[var(--dg-text-primary)]'}`}>{fdInfo.usage_pct != null ? `${fdInfo.usage_pct.toFixed(1)}%` : '—'}</span>],
].map(([label, val]) => (
<div key={label as string}>
<span className="text-[var(--dg-text-muted)]">{label}:</span>
{val}
</div>
))}
{csInfo?.ctx_per_sec != null && [
['CTX 累计', <span className="font-mono">{(csInfo.ctx_per_sec / 1e6).toFixed(1)}M</span>],
['IRQ 累计', <span className="font-mono">{(csInfo.irq_per_sec / 1e6).toFixed(1)}M</span>],
].map(([label, val]) => (
<div key={label as string}>
<span className="text-[var(--dg-text-muted)]">{label}:</span>
{val}
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* 磁盘分区 + 网络接口 (保持原样) */}
{latest?.disk_info && (() => {
const disk = parseJSON(latest.disk_info);
@@ -838,20 +929,25 @@ export function HostMonitor() {
</p>
</div>
{/* 端口白名单 */}
{/* 端口白名单 — 仅黑名单模式可编辑 */}
<div>
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1.5 block">
端口列表 <span className="text-[var(--dg-text-muted)]">(逗号分隔,如: 80,443,3306,6379)</span>
{portPolicyMode === 'whitelist' && <span className="text-blue-400 ml-1">— 白名单模式无需配置</span>}
</label>
<textarea
className="w-full h-20 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 py-1.5 text-xs font-mono text-[var(--dg-text-primary)] resize-none"
className={`w-full h-20 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 py-1.5 text-xs font-mono resize-none ${
portPolicyMode === 'whitelist' ? 'text-[var(--dg-text-muted)] opacity-50 cursor-not-allowed' : 'text-[var(--dg-text-primary)]'
}`}
value={portWhitelist.join(',')}
onChange={e => {
if (portPolicyMode === 'whitelist') return;
const val = e.target.value;
const nums = val.split(',').map(s => parseInt(s.trim())).filter(n => !isNaN(n) && n > 0 && n <= 65535);
setPortWhitelist(nums);
}}
placeholder="80,443,3306,6379"
placeholder={portPolicyMode === 'whitelist' ? '白名单模式下所有端口自动上报' : '80,443,3306,6379'}
readOnly={portPolicyMode === 'whitelist'}
/>
<p className="text-[10px] text-[var(--dg-text-muted)] mt-1">
已输入 {portWhitelist.length} 个端口