fix: deduplicate filesystem mounts in monitoring
This commit is contained in:
@@ -176,6 +176,9 @@
|
||||
.system-monitoring-metric small { color: var(--color-text-muted); font-size: 12px; }
|
||||
.system-monitoring-metric strong { color: var(--color-text-strong); font-size: 23px; line-height: 1.25; }
|
||||
.system-monitoring-metric small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.system-monitoring-metric__mounts { color: var(--color-text-muted); font-size: 11px; margin-top: 3px; }
|
||||
.system-monitoring-metric__mounts summary { cursor: pointer; }
|
||||
.system-monitoring-metric__mounts ul { margin: 5px 0 0; padding-left: 16px; overflow-wrap: anywhere; }
|
||||
|
||||
.system-monitoring-main-grid {
|
||||
align-items: start;
|
||||
|
||||
@@ -61,6 +61,26 @@ function formatRate(value: number | null) {
|
||||
return value === null ? '—' : `${formatBytes(value)}/s`;
|
||||
}
|
||||
|
||||
export function DiskMetricCards({ disks }: { disks: InfrastructureMonitoringOverview['disks'] }) {
|
||||
if (!disks.length) return <article className="surface system-monitoring-metric"><HardDrive size={19} /><div><span>磁盘</span><strong>暂无数据</strong></div></article>;
|
||||
return <>{disks.map((disk) => {
|
||||
const aliases = (disk.mountpoints ?? [disk.mountpoint]).filter((path) => path !== disk.mountpoint);
|
||||
return <article className="surface system-monitoring-metric" key={disk.id}>
|
||||
<div className="system-monitoring-metric__icon is-amber"><HardDrive size={19} /></div>
|
||||
<div>
|
||||
<span>{disk.mountpoint === '/' ? '系统盘' : '磁盘'} {disk.mountpoint}</span>
|
||||
<strong>{formatPercent(disk.usagePercent)}</strong>
|
||||
<small title={`${disk.device} · ${disk.filesystem} · ${disk.instance}`}>{disk.device} · {disk.filesystem}</small>
|
||||
<small>{formatBytes(disk.availableBytes)} 可用 / {formatBytes(disk.totalBytes)}</small>
|
||||
{aliases.length > 0 ? <details className="system-monitoring-metric__mounts">
|
||||
<summary>其他挂载点({aliases.length})</summary>
|
||||
<ul>{aliases.map((path) => <li key={path}>{path}</li>)}</ul>
|
||||
</details> : null}
|
||||
</div>
|
||||
</article>;
|
||||
})}</>;
|
||||
}
|
||||
|
||||
function totalNetworkRate(receive: number | null | undefined, transmit: number | null | undefined) {
|
||||
if (receive === null || receive === undefined || transmit === null || transmit === undefined) return null;
|
||||
return receive + transmit;
|
||||
@@ -352,11 +372,7 @@ export function AdminSystemMonitoringPage() {
|
||||
<div className="system-monitoring-metrics">
|
||||
<article className="surface system-monitoring-metric"><div className="system-monitoring-metric__icon is-blue"><Cpu size={19} /></div><div><span>CPU 使用率</span><strong>{formatPercent(metrics?.cpuUsagePercent ?? null)}</strong><small>5分钟平均</small></div></article>
|
||||
<article className="surface system-monitoring-metric"><div className="system-monitoring-metric__icon is-violet"><MemoryStick size={19} /></div><div><span>内存使用率</span><strong>{formatPercent(metrics?.memoryUsagePercent ?? null)}</strong><small>{formatBytes(metrics?.memoryAvailableBytes ?? null)} 可用 / {formatBytes(metrics?.memoryTotalBytes ?? null)}</small></div></article>
|
||||
{(overview?.disks ?? []).map((disk) => <article className="surface system-monitoring-metric" key={disk.id}>
|
||||
<div className="system-monitoring-metric__icon is-amber"><HardDrive size={19} /></div>
|
||||
<div><span>{disk.mountpoint === '/' ? '系统盘' : '磁盘'} {disk.mountpoint}</span><strong>{formatPercent(disk.usagePercent)}</strong><small title={`${disk.device} · ${disk.filesystem} · ${disk.instance}`}>{disk.device} · {disk.filesystem}</small><small>{formatBytes(disk.availableBytes)} 可用 / {formatBytes(disk.totalBytes)}</small></div>
|
||||
</article>)}
|
||||
{!overview?.disks?.length ? <article className="surface system-monitoring-metric"><HardDrive size={19} /><div><span>磁盘</span><strong>暂无数据</strong></div></article> : null}
|
||||
<DiskMetricCards disks={overview?.disks ?? []} />
|
||||
<article className="surface system-monitoring-metric"><div className="system-monitoring-metric__icon is-green"><Network size={19} /></div><div><span>网络吞吐</span><strong>{formatRate(totalNetworkRate(metrics?.networkReceiveBytesPerSecond, metrics?.networkTransmitBytesPerSecond))}</strong><small>接收 {formatRate(metrics?.networkReceiveBytesPerSecond ?? null)} · 发送 {formatRate(metrics?.networkTransmitBytesPerSecond ?? null)}</small></div></article>
|
||||
</div>
|
||||
|
||||
@@ -364,7 +380,7 @@ export function AdminSystemMonitoringPage() {
|
||||
<div className="system-monitoring-chart-stack">
|
||||
<article className="surface system-monitoring-chart-card"><header><div><Cpu size={17} /><strong>CPU 趋势</strong></div><span>{formatPercent(metrics?.cpuUsagePercent ?? null)}</span></header>{overview?.trends.cpuUsagePercent.length ? <Chart height={230} option={cpuOption} /> : <EmptyChart />}</article>
|
||||
<article className="surface system-monitoring-chart-card"><header><div><MemoryStick size={17} /><strong>内存趋势</strong></div><span>{formatPercent(metrics?.memoryUsagePercent ?? null)}</span></header>{overview?.trends.memoryUsagePercent.length ? <Chart height={230} option={memoryOption} /> : <EmptyChart />}</article>
|
||||
<article className="surface system-monitoring-chart-card"><header><div><HardDrive size={17} /><strong>全部磁盘趋势</strong></div><span>{overview?.disks?.length ?? 0} 个挂载点</span></header>{overview?.disks?.some((disk) => disk.trend.length) ? <Chart height={230} option={diskOption} /> : <EmptyChart />}</article>
|
||||
<article className="surface system-monitoring-chart-card"><header><div><HardDrive size={17} /><strong>全部磁盘趋势</strong></div><span>{overview?.disks?.length ?? 0} 个文件系统</span></header>{overview?.disks?.some((disk) => disk.trend.length) ? <Chart height={230} option={diskOption} /> : <EmptyChart />}</article>
|
||||
<article className="surface system-monitoring-chart-card"><header><div><Activity size={17} /><strong>网络趋势</strong></div><span>{formatRate(totalNetworkRate(metrics?.networkReceiveBytesPerSecond, metrics?.networkTransmitBytesPerSecond))}</span></header>{overview?.trends.networkReceiveBytesPerSecond.length ? <Chart height={230} option={networkOption} /> : <EmptyChart />}</article>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { InfrastructureMonitoringOverview } from '@/api/adminApi';
|
||||
import { DiskMetricCards } from './AdminSystemMonitoringPage';
|
||||
|
||||
vi.mock('@/components/ui/Chart', () => ({ Chart: () => null }));
|
||||
|
||||
const disk: InfrastructureMonitoringOverview['disks'][number] = {
|
||||
id: '["host:9100","/dev/sdb1","ext4"]', instance: 'host:9100', device: '/dev/sdb1',
|
||||
filesystem: 'ext4', mountpoint: '/data', mountpoints: ['/data', '/var/lib/minio', '/var/lib/pgsql', '/var/lib/redis'],
|
||||
totalBytes: 100 * 1024 ** 3, availableBytes: 90 * 1024 ** 3, usagePercent: 10, trend: [],
|
||||
};
|
||||
|
||||
describe('filesystem metric cards', () => {
|
||||
it('shows one capacity card and expandable alias paths for a bind-mounted filesystem', async () => {
|
||||
render(<DiskMetricCards disks={[disk]} />);
|
||||
expect(screen.getAllByRole('article')).toHaveLength(1);
|
||||
expect(screen.getByText('磁盘 /data')).toBeInTheDocument();
|
||||
expect(screen.getByText('90.0 GB 可用 / 100.0 GB')).toBeInTheDocument();
|
||||
const summary = screen.getByText('其他挂载点(3)');
|
||||
expect(summary.closest('details')).not.toHaveAttribute('open');
|
||||
await userEvent.click(summary);
|
||||
expect(summary.closest('details')).toHaveAttribute('open');
|
||||
for (const path of disk.mountpoints.slice(1)) expect(screen.getByText(path)).toBeVisible();
|
||||
});
|
||||
|
||||
it('keeps the root filesystem distinct and does not invent aliases or missing metrics', () => {
|
||||
render(<DiskMetricCards disks={[disk, { ...disk, id: 'root', device: '/dev/sda2', mountpoint: '/', mountpoints: ['/'], usagePercent: null }]} />);
|
||||
expect(screen.getAllByRole('article')).toHaveLength(2);
|
||||
expect(screen.getByText('系统盘 /')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('其他挂载点(3)')).toHaveLength(1);
|
||||
expect(screen.getByText('—')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows an explicit unavailable state without stale disk cards', () => {
|
||||
const { rerender } = render(<DiskMetricCards disks={[disk]} />);
|
||||
rerender(<DiskMetricCards disks={[]} />);
|
||||
expect(screen.getByText('暂无数据')).toBeInTheDocument();
|
||||
expect(screen.queryByText('磁盘 /data')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user