From 7234fbe71f4bfbf1a4cbfc5661515e673dfed9a4 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 9 Jun 2026 16:19:54 +0100 Subject: [PATCH] Keep a guest's host node visible when searching the Proxmox overview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Proxmox overview's shared search box is a VM/LXC filter, but filteredNodes applied the same term to the nodes table independently. Searching a guest name (e.g. 'debian-go') matched no node name, so the nodes table collapsed to the 'No Proxmox VE nodes' empty state — which reads as 'you have no Proxmox infrastructure' even though the matching guest was listed right below it on its host node. Extract filterProxmoxNodesForSearch: keep a node when it matches the term directly OR when it hosts a guest that matches the term. A guest search now keeps that guest's host node visible for context; a node-name search still narrows to the matching node; an empty term still shows every node. Regression tests cover all three cases; verified live (searching 'debian-go' now shows the minipc host node instead of the empty state). --- .../features/proxmox/ProxmoxPageSurface.tsx | 14 ++++--- .../__tests__/proxmoxPageModel.test.ts | 39 +++++++++++++++++++ .../src/features/proxmox/proxmoxPageModel.ts | 27 +++++++++++++ 3 files changed, 74 insertions(+), 6 deletions(-) diff --git a/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx b/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx index e80756ebd..b74e461b8 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx @@ -23,7 +23,6 @@ import { getPlatformIcon } from '@/features/platformPage/platformIcon'; import { PlatformOutdatedAgentNotice } from '@/features/platformPage/PlatformOutdatedAgentNotice'; import { usePersistentSignal } from '@/hooks/usePersistentSignal'; import { STORAGE_KEYS } from '@/utils/localStorage'; -import { resourceMatchesSearch } from '@/utils/resourceSearchMatch'; import { PlatformErrorState, PlatformSectionTabs, @@ -41,6 +40,7 @@ import { PROXMOX_TAB_SPECS, buildProxmoxPageModel, buildVisibleProxmoxTabSpecs, + filterProxmoxNodesForSearch, type ProxmoxPageModel, type ProxmoxPageTabId, } from './proxmoxPageModel'; @@ -247,11 +247,13 @@ function ProxmoxOverview(props: ProxmoxOverviewProps) { workloadsState.surfaceInitialDataReceived() && workloadsState.allGuests().length > 0, ); - const filteredNodes = createMemo(() => { - const term = workloadsState.search(); - if (!term.trim()) return props.model().pveNodes; - return props.model().pveNodes.filter((node) => resourceMatchesSearch(node, term)); - }); + const filteredNodes = createMemo(() => + filterProxmoxNodesForSearch( + props.model().pveNodes, + props.model().guests, + workloadsState.search(), + ), + ); return (
diff --git a/frontend-modern/src/features/proxmox/__tests__/proxmoxPageModel.test.ts b/frontend-modern/src/features/proxmox/__tests__/proxmoxPageModel.test.ts index ff9f8664a..5f2fd436d 100644 --- a/frontend-modern/src/features/proxmox/__tests__/proxmoxPageModel.test.ts +++ b/frontend-modern/src/features/proxmox/__tests__/proxmoxPageModel.test.ts @@ -4,6 +4,7 @@ import { PROXMOX_TAB_SPECS, buildProxmoxPageModel, buildVisibleProxmoxTabSpecs, + filterProxmoxNodesForSearch, getResourceVersion, resolveProxmoxPlatformScope, } from '../proxmoxPageModel'; @@ -31,6 +32,44 @@ describe('proxmoxPageModel', () => { ]); }); + describe('filterProxmoxNodesForSearch', () => { + const minipc = makeResource({ + id: 'minipc', + type: 'agent', + proxmox: { nodeName: 'minipc', clusterName: 'homelab' }, + }); + const delly = makeResource({ + id: 'delly', + type: 'agent', + proxmox: { nodeName: 'delly', clusterName: 'homelab' }, + }); + const debianGo = makeResource({ + id: 'system-container-112', + type: 'system-container', + name: 'debian-go', + proxmox: { vmid: 112, nodeName: 'minipc' }, + }); + + it('returns every node when the search term is empty', () => { + expect(filterProxmoxNodesForSearch([minipc, delly], [debianGo], '')).toEqual([ + minipc, + delly, + ]); + }); + + it('keeps the host node of a matching guest so a guest search does not empty the nodes table', () => { + // Regression: searching a guest name used to filter the nodes table to + // nothing, surfacing the "No Proxmox VE nodes" empty state even though the + // guest's host node exists and the guest is listed below. + const result = filterProxmoxNodesForSearch([minipc, delly], [debianGo], 'debian-go'); + expect(result).toEqual([minipc]); + }); + + it('still matches a node by its own name', () => { + expect(filterProxmoxNodesForSearch([minipc, delly], [debianGo], 'delly')).toEqual([delly]); + }); + }); + it('builds a Proxmox-first estate model from canonical v6 resources', () => { const model = buildProxmoxPageModel([ makeResource({ diff --git a/frontend-modern/src/features/proxmox/proxmoxPageModel.ts b/frontend-modern/src/features/proxmox/proxmoxPageModel.ts index a67cdfc71..5212d109a 100644 --- a/frontend-modern/src/features/proxmox/proxmoxPageModel.ts +++ b/frontend-modern/src/features/proxmox/proxmoxPageModel.ts @@ -1,6 +1,7 @@ import type { Resource, ResourceMetric, ResourceType } from '@/types/resource'; import type { ResourceChange } from '@/types/resource'; import { formatProxmoxVersion } from '@/utils/proxmoxVersion'; +import { resourceMatchesSearch } from '@/utils/resourceSearchMatch'; export type ProxmoxPageTabId = 'overview' | 'storage' | 'replication' | 'backups' | 'ceph' | 'mail'; @@ -175,6 +176,32 @@ export function getResourceNodeName(resource: Resource): string { ); } +// filterProxmoxNodesForSearch narrows the nodes table to match the shared +// workload search box. Because that box is a VM/LXC filter, filtering nodes by +// the raw term alone collapses the table to its empty state whenever the term +// matches a guest but not a node name — which misreads as "no Proxmox nodes" +// while the matching guest is listed right below. Keep a node when it matches +// the term directly OR when it hosts a guest that matches, so a guest search +// still shows that guest's host node for context. +export function filterProxmoxNodesForSearch( + nodes: Resource[], + guests: Resource[], + term: string, +): Resource[] { + if (!term.trim()) return nodes; + const matchingGuestNodeNames = new Set( + guests + .filter((guest) => resourceMatchesSearch(guest, term)) + .map((guest) => getResourceNodeName(guest)) + .filter(Boolean), + ); + return nodes.filter( + (node) => + resourceMatchesSearch(node, term) || + matchingGuestNodeNames.has(getResourceNodeName(node)), + ); +} + export function getResourceVmid(resource: Resource): string { const vmid = resource.proxmox?.vmid; if (typeof vmid === 'number' && Number.isFinite(vmid)) {