fix: HostMonitor 下拉主机名修复 - 补回 setHosts(list)
This commit is contained in:
1 parent
8240a88a84
commit
f061785266
1 file changed
+139
-43
@@ -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 文件句柄 & 上下文切换</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} 个端口
|
||||
|
||||
Reference in new issue
Block a user