Align Proxmox totals with filtered workloads

This commit is contained in:
rcourtman
2026-08-02 21:55:18 +01:00
parent c143a7cf32
commit bacff3e2eb
6 changed files with 68 additions and 32 deletions
@@ -3902,6 +3902,10 @@ navigation (`pulse:filterbar:saved-views:<key>`); `SavedViewsMenu` owns
the dropdown chrome. A "view" is the page's URL query string at save
time, so saved views double as shareable links: copying the bar URL
after applying a view gives someone else the exact filtered state.
Counts or orientation strips presented as part of a filtered resource table
must derive from that table's canonical filtered collection as well. They must
not continue showing page-wide inventory totals after FilterBar state, saved
views, or search has narrowed the rows the operator can see.
Implicit "remember last filters" is intentionally not added — defaulting
to yesterday's filter state on a monitoring page hides real problems.
That same shared filter-toolbar boundary also owns controlled select continuity
@@ -468,6 +468,11 @@ presentation only. Comparing a VM or LXC's used bytes with its parent-node
memory total may change the memory bar and memory sort order, but it must not
change Backup-column freshness, protection coverage, restore-point
correlation, storage-health state, or recovery authority.
The adjacent Proxmox overview guest-totals strip is likewise presentation-only
and must consume the embedded Workloads surface's filtered statistics. It must
not fall back to page-wide guest summary counts when search, type, status, or
node filters have narrowed the visible workload inventory; backup and recovery
authority remain unchanged by either set of presentation counts.
That overview Backup signal belongs to Proxmox VMs and LXCs. If the embedded
Workloads table demotes Docker-in-LXC `app-container` rows out of peer
membership, the backup surface must still receive the page model's Proxmox
+14 -22
View File
@@ -1,13 +1,13 @@
{
"version": 1,
"base_sha": "b3dbc26d545b7526f62ed466884c4006f07cf8cb",
"verified_at": "2026-08-02T20:27:26Z",
"base_sha": "c143a7cf322b407e8091ca4e27a16e92a5178cef",
"verified_at": "2026-08-02T20:53:27Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/shared/searchInputEnhancementsModel.ts"
"frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx"
],
"content_sha256": {
"frontend-modern/src/components/shared/searchInputEnhancementsModel.ts": "cd34929cd632fcf6eda163b34def13df0666d91f279e5df9e329ee8b0b05ee33"
"frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx": "3dafdf4fec6e6843082f347258c7913f96bc1bf9fc23b10e2461d87ffa90110f"
},
"routes": [
"/proxmox/overview"
@@ -17,31 +17,23 @@
"width": 1280,
"height": 900
},
{
"width": 640,
"height": 800
},
{
"width": 390,
"height": 844
}
],
"states": [
"Desktop default filter toolbar",
"Desktop empty search-history menu",
"Desktop populated search-history menu",
"Desktop active search with history closed",
"Tablet-width empty search-history menu",
"Phone-width populated search-history menu",
"Restored empty search and cleared history"
"Desktop filtered saved view with one stopped VM",
"Phone-width filtered saved view with one stopped VM",
"Desktop zero-result Attention filter",
"Desktop unfiltered workload inventory",
"Clean unfiltered state after saved-view removal and reload"
],
"interactions": [
"Opened the empty search-history menu and confirmed it remained bounded to 512px on the 1280px desktop viewport",
"Confirmed the same menu used the full available search width at 640px and 390px",
"Measured zero document-level horizontal overflow at all three verified widths",
"Entered edge, committed it with Enter, and confirmed the search filtered the workload table",
"Reopened history and confirmed the populated entry and clear-history action rendered within the bounded menu",
"Closed history and confirmed the active-search filter toolbar remained usable",
"Cleared history and the active search, then confirmed the overview returned to its clean default state"
"Confirmed the one-row VMs, Stopped, and Node pve1 view rendered 0 running and 1 stopped instead of estate-wide totals",
"Inspected the filtered totals strip at 1280x900 and 390x844 for alignment, clipping, and horizontal overflow",
"Cleared the saved filters and confirmed the totals returned to the full visible workload collection",
"Applied the Attention filter and confirmed the totals strip was absent beside the canonical zero-result state",
"Removed the temporary saved view, cleared filters, reloaded the route, and confirmed no saved default or filter state remained"
]
}
@@ -299,6 +299,7 @@ function ProxmoxOverview(props: ProxmoxOverviewProps) {
workloadsState.surfaceInitialDataReceived() &&
workloadsState.allGuests().length > 0,
);
const visibleGuestStats = createMemo(() => workloadsState.totalStats());
const filteredNodes = createMemo(() =>
filterProxmoxNodesForSearch(
currentModel().pveNodes,
@@ -373,28 +374,28 @@ function ProxmoxOverview(props: ProxmoxOverviewProps) {
emptyStateTitle="No Proxmox workloads"
emptyStateDescription="Proxmox VMs and LXCs appear here when inventory is available."
/>
{/* v5 dashboard closed with a running/stopped totals strip; the counts
are platform-wide, matching the status-chip vocabulary above. */}
<Show when={currentModel().summary.guestCount > 0}>
{/* Keep this orientation strip aligned with the rows the operator can
currently see rather than the unfiltered page-wide inventory. */}
<Show when={visibleGuestStats().total > 0}>
<div
class="flex items-center gap-2 rounded border border-border bg-surface-alt px-2 py-1 text-xs text-muted"
data-testid="proxmox-guest-totals"
>
<span class="flex items-center gap-1.5">
<StatusDot size="xs" variant="success" ariaHidden />
{currentModel().summary.runningGuestCount} running
{visibleGuestStats().running} running
</span>
<Show when={currentModel().summary.degradedGuestCount > 0}>
<Show when={visibleGuestStats().degraded > 0}>
<span aria-hidden="true">|</span>
<span class="flex items-center gap-1.5">
<StatusDot size="xs" variant="warning" ariaHidden />
{currentModel().summary.degradedGuestCount} attention
{visibleGuestStats().degraded} attention
</span>
</Show>
<span aria-hidden="true">|</span>
<span class="flex items-center gap-1.5">
<StatusDot size="xs" variant="danger" ariaHidden />
{currentModel().summary.stoppedGuestCount} stopped
{visibleGuestStats().stopped} stopped
</span>
</div>
</Show>
@@ -377,6 +377,18 @@ describe('ProxmoxBackupsTable', () => {
expect(proxmoxPageSurfaceSource).not.toContain('workloads={workloadsState.allGuests');
});
it('keeps Overview guest totals aligned with the filtered Workloads collection', () => {
expect(proxmoxPageSurfaceSource).toContain(
'const visibleGuestStats = createMemo(() => workloadsState.totalStats())',
);
expect(proxmoxPageSurfaceSource).not.toContain(
'currentModel().summary.runningGuestCount} running',
);
expect(proxmoxPageSurfaceSource).not.toContain(
'currentModel().summary.stoppedGuestCount} stopped',
);
});
it('keeps the shared storage surface scoped to the whole Proxmox product family', () => {
expect(proxmoxPageSurfaceSource).toContain("const PROXMOX_PLATFORM_FILTER = 'proxmox-all';");
expect(proxmoxPageSurfaceSource).toContain('forcedSourceFilter={PROXMOX_PLATFORM_FILTER}');
@@ -7,6 +7,7 @@ const mockUseUnifiedResources = vi.fn();
const mockPathname = vi.hoisted(() => vi.fn(() => '/proxmox/overview'));
const mockVersionInfo = vi.hoisted(() => vi.fn());
const mockStorageProps = vi.hoisted(() => vi.fn());
const mockTotalStats = vi.hoisted(() => vi.fn());
const makeResource = (resource: Partial<Resource> & Pick<Resource, 'id' | 'type'>): Resource =>
({
@@ -81,6 +82,7 @@ vi.mock('@/components/Workloads/useWorkloadsState', () => ({
surfaceConnected: () => false,
surfaceInitialDataReceived: () => false,
allGuests: () => [],
totalStats: mockTotalStats,
search: () => '',
setSearch: vi.fn(),
}),
@@ -129,6 +131,16 @@ describe('ProxmoxPageSurface contract', () => {
beforeEach(() => {
mockPathname.mockReturnValue('/proxmox/overview');
mockVersionInfo.mockReturnValue(null);
mockTotalStats.mockReturnValue({
total: 3,
running: 1,
degraded: 1,
stopped: 1,
vms: 3,
containers: 0,
appContainers: 0,
pods: 0,
});
});
afterEach(() => {
@@ -186,7 +198,7 @@ describe('ProxmoxPageSurface contract', () => {
);
});
it('renders the v5-style guest totals strip from the page summary', () => {
it('renders guest totals from the filtered workload collection', () => {
setResources([
makeResource({
id: 'agent:pve-1',
@@ -212,12 +224,22 @@ describe('ProxmoxPageSurface contract', () => {
proxmox: { nodeName: 'pve-1', vmid: 102 },
}),
]);
mockTotalStats.mockReturnValue({
total: 1,
running: 0,
degraded: 0,
stopped: 1,
vms: 1,
containers: 0,
appContainers: 0,
pods: 0,
});
render(() => <ProxmoxPageSurface />);
const totals = screen.getByTestId('proxmox-guest-totals');
expect(totals).toHaveTextContent('1 running');
expect(totals).toHaveTextContent('1 attention');
expect(totals).toHaveTextContent('0 running');
expect(totals).not.toHaveTextContent('attention');
expect(totals).toHaveTextContent('1 stopped');
});