fix: deduplicate filesystem mounts in monitoring

This commit is contained in:
hectorzhao
2026-08-31 23:39:24 +08:00
parent 12668cee87
commit 1f2dfb5caf
14 changed files with 246 additions and 51 deletions
@@ -46,6 +46,7 @@ export type InfrastructureMonitoringOverview = {
instance: string;
device: string;
mountpoint: string;
mountpoints: string[];
filesystem: string;
usagePercent: number | null;
totalBytes: number | null;
@@ -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();
});
});