fix(frontend): hide unavailable drawer history metrics

This commit is contained in:
rcourtman
2026-08-25 20:15:59 +01:00
parent 3678d583bf
commit 393016c509
5 changed files with 67 additions and 18 deletions
@@ -703,7 +703,9 @@ change may globally weaken the Task 03 lifecycle-state idempotency invariant.
accumulating, but it must never be expanded into synthetic timestamps or a
flat line. Fewer than two stored points remains `Collecting history` and
only metrics-store samples may contribute chart geometry, bounds, or hover
inspection.
inspection. Current-value legends must preserve the same availability
semantics as workload rows: the poller's negative unknown-disk sentinel is
unavailable and renders `-`, never a fabricated negative percentage.
History cards must let the plot area stretch to the card height instead of
pinning the SVG wrapper to a fixed short height, so guest and node drawer
history cards do not leave unused card space when headers have different
+20 -15
View File
@@ -1,30 +1,35 @@
{
"version": 1,
"base_sha": "caf7f80867871f0a601d9a572cbab309f8d4b41f",
"verified_at": "2026-08-25T18:42:00Z",
"base_sha": "3678d583bf43840d0086e4ba70c81aec77dadd01",
"verified_at": "2026-08-25T19:06:05Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/AI/AICostDashboard.tsx",
"frontend-modern/src/utils/aiCostPresentation.ts"
"frontend-modern/src/components/Workloads/guestDrawerModel.ts"
],
"content_sha256": {
"frontend-modern/src/components/AI/AICostDashboard.tsx": "eb9291049d9c415bfc24868cf45da5f9d862bb24f98100bc0fb10e2188554100",
"frontend-modern/src/utils/aiCostPresentation.ts": "816376697dc3eefa1b3d3a198ab29f80ae9148ddbffaf3dc1ab18fb22bc46041"
"frontend-modern/src/components/Workloads/guestDrawerModel.ts": "5aaccadfaa746f391431c5138e7a1c051b643e264e57fb8e2f58f121cffb0de9"
},
"routes": ["/settings/pulse-intelligence/provider"],
"routes": [
"/proxmox/overview",
"/docker/overview",
"/kubernetes/overview",
"/truenas/apps"
],
"viewports": [
{ "width": 1440, "height": 900 },
{ "width": 1280, "height": 800 },
{ "width": 390, "height": 844 }
],
"states": [
"loaded provider usage range with no provider/model rows",
"phone projection retaining Provider, Model, and Est. USD",
"desktop projection restoring Input, Output, and Total"
"stopped Proxmox guest History drawer using the negative unknown-disk sentinel",
"running Docker container History drawer with populated utilization data",
"degraded Kubernetes pod History drawer with populated utilization and network data",
"TrueNAS app Overview drawer with long container and identity values"
],
"interactions": [
"navigated to Provider & Models and scrolled the settings surface to Provider Usage & Spend",
"verified the empty-state row is fully readable at 390px after correcting fixed-table text clipping",
"measured zero table-shell and page overflow at 390px and 1440px",
"confirmed all six desktop headers return at 1440px"
"opened the stopped dev-portal Proxmox guest and switched from Overview to History at 390px",
"verified the unavailable disk fallback renders as a dash instead of a negative percentage at 390px and 1280px",
"opened Docker and Kubernetes History tabs to confirm valid current metrics and chart geometry remain intact",
"opened a TrueNAS app drawer and inspected long-value truncation, stacking, scrolling, and tab placement",
"restored the browser viewport after responsive verification"
]
}
@@ -502,6 +502,33 @@ describe('GuestDrawer', () => {
expect(utilizationChart.querySelector('path')).toBeNull();
});
it('renders unavailable current disk usage as a dash', async () => {
chartsApiMocks.getMetricsHistory.mockResolvedValue({
resourceType: 'vm',
resourceId: 'inst1:node1:100',
range: '24h',
start: 0,
end: 0,
metrics: {},
source: 'store',
});
render(() => (
<GuestDrawer
guest={makeGuest({
disk: { total: 100, used: 0, free: 100, usage: -1 },
})}
onClose={vi.fn()}
/>
));
await fireEvent.click(screen.getByText('History'));
await waitFor(() => expect(chartsApiMocks.getMetricsHistory).toHaveBeenCalled());
const utilizationChart = screen.getAllByTestId('guest-history-group-chart')[0];
expect(utilizationChart).toHaveTextContent('Disk-');
expect(utilizationChart).not.toHaveTextContent('Disk-100.0%');
});
it('updates grouped metric header values on History chart hover', async () => {
render(() => <GuestDrawer guest={makeGuest()} onClose={vi.fn()} />);
@@ -142,6 +142,15 @@ describe('guestDrawerModel (branch coverage)', () => {
expect(result.memory).toBeUndefined();
});
it('does not render the negative unknown-disk sentinel as a current history value', () => {
const result = getGuestDrawerCurrentMetrics(
makeGuest({
disk: { total: 100, used: 0, free: 100, usage: -1 },
}),
);
expect(result.disk).toBeUndefined();
});
it('drops a non-finite network value via the finite() guard', () => {
const result = getGuestDrawerCurrentMetrics(
makeGuest({ networkIn: Number.POSITIVE_INFINITY }),
@@ -73,13 +73,19 @@ export const getGuestDrawerCurrentMetrics = (guest: Guest): Record<string, numbe
const cpuPercent = available('cpu') ? getWorkloadCPUPercent(guest.cpu) : undefined;
const memUsage =
available('memory') && !guest.memory?.usageUnavailable ? guest.memory?.usage : undefined;
const diskUsage = available('disk') ? guest.disk?.usage : undefined;
const reportedDiskUsage = available('disk') ? guest.disk?.usage : undefined;
const diskUsage =
typeof reportedDiskUsage === 'number' &&
Number.isFinite(reportedDiskUsage) &&
reportedDiskUsage >= 0
? reportedDiskUsage
: undefined;
const finite = (value: number | undefined): number | undefined =>
typeof value === 'number' && Number.isFinite(value) ? value : undefined;
return {
cpu: finite(cpuPercent),
memory: finite(memUsage),
disk: finite(diskUsage),
disk: diskUsage,
netin: available('networkIO') ? finite(guest.networkIn) : undefined,
netout: available('networkIO') ? finite(guest.networkOut) : undefined,
diskread: available('diskIO') ? finite(guest.diskRead) : undefined,