From 5180b84d4e6c4a96c124eebd0245ddf231f0f930 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 30 Sep 2025 16:50:51 +0000 Subject: [PATCH] fix: resolve grouping issues when nodes have duplicate hostnames addresses #476 when multiple nodes have the same hostname, the dashboard and storage views were incorrectly grouping VMs/containers/storage by hostname instead of by unique node instance ID. this caused: - incorrect VM/container counts in node summary - mixed display of resources from different nodes - incorrect grouping in storage view changed grouping logic to use guest.instance (unique node ID) instead of guest.node (hostname). updated both Dashboard and Storage components to properly map instance IDs to node objects for display while maintaining correct data separation. --- .../src/components/Dashboard/Dashboard.tsx | 49 ++++++----- .../src/components/Storage/Storage.tsx | 82 +++++++++++-------- 2 files changed, 73 insertions(+), 58 deletions(-) diff --git a/frontend-modern/src/components/Dashboard/Dashboard.tsx b/frontend-modern/src/components/Dashboard/Dashboard.tsx index 0e09784a1..511465869 100644 --- a/frontend-modern/src/components/Dashboard/Dashboard.tsx +++ b/frontend-modern/src/components/Dashboard/Dashboard.tsx @@ -98,17 +98,16 @@ export function Dashboard(props: DashboardProps) { } }); - // Create a mapping from node name to host URL - const nodeHostMap = createMemo(() => { - const map: Record = {}; + // Create a mapping from node instance ID to node object + const nodeByInstance = createMemo(() => { + const map: Record = {}; props.nodes.forEach((node) => { - if (node.host) { - map[node.name] = node.host; - } + map[node.id] = node; }); return map; }); + // Persist filter states to localStorage createEffect(() => { localStorage.setItem('dashboardViewMode', viewMode()); @@ -356,13 +355,14 @@ export function Dashboard(props: DashboardProps) { return groups; } - // Original grouped by node logic + // Group by node instance ID (not hostname) to handle nodes with duplicate names const groups: Record = {}; guests.forEach((guest) => { - if (!groups[guest.node]) { - groups[guest.node] = []; + const instanceId = guest.instance; // Use unique instance ID instead of hostname + if (!groups[instanceId]) { + groups[instanceId] = []; } - groups[guest.node].push(guest); + groups[instanceId].push(guest); }); // Sort within each node group @@ -557,10 +557,10 @@ export function Dashboard(props: DashboardProps) { const diskPercent = () => node.disk ? Math.round((node.disk.used / node.disk.total) * 100) : 0; - // Count VMs and containers for this node - const nodeVMs = () => props.vms.filter((vm) => vm.node === node.name).length; + // Count VMs and containers for this node (use instance ID to handle duplicate node names) + const nodeVMs = () => props.vms.filter((vm) => vm.instance === node.id).length; const nodeContainers = () => - props.containers.filter((ct) => ct.node === node.name).length; + props.containers.filter((ct) => ct.instance === node.id).length; const isSelected = () => search().includes(`node:${node.name}`); @@ -915,25 +915,29 @@ export function Dashboard(props: DashboardProps) { a.localeCompare(b))} + each={Object.entries(groupedGuests()).sort(([instanceIdA], [instanceIdB]) => { + // Sort by node name for display, not instance ID + const nodeA = nodeByInstance()[instanceIdA]; + const nodeB = nodeByInstance()[instanceIdB]; + return (nodeA?.name || '').localeCompare(nodeB?.name || ''); + })} fallback={<>} > - {([node, guests]) => ( + {([instanceId, guests]) => { + const node = nodeByInstance()[instanceId]; + return ( <> - {node} + {node.name} @@ -962,7 +966,8 @@ export function Dashboard(props: DashboardProps) { )} - )} + ); + }} diff --git a/frontend-modern/src/components/Storage/Storage.tsx b/frontend-modern/src/components/Storage/Storage.tsx index a3738561b..30ae9b23b 100644 --- a/frontend-modern/src/components/Storage/Storage.tsx +++ b/frontend-modern/src/components/Storage/Storage.tsx @@ -20,17 +20,16 @@ const Storage: Component = () => { const [sortKey, setSortKey] = createSignal('name'); const [sortDirection, setSortDirection] = createSignal<'asc' | 'desc'>('asc'); - // Create a mapping from node name to host URL - const nodeHostMap = createMemo(() => { - const map: Record = {}; + // Create a mapping from node instance ID to node object + const nodeByInstance = createMemo(() => { + const map: Record = {}; (state.nodes || []).forEach((node) => { - if (node.host) { - map[node.name] = node.host; - } + map[node.id] = node; }); return map; }); + const sortKeyOptions: { value: StorageSortKey; label: string }[] = [ { value: 'name', label: 'Name' }, { value: 'node', label: 'Node' }, @@ -243,10 +242,12 @@ const Storage: Component = () => { const mode = viewMode(); if (mode === 'node') { + // Group by instance ID (not node name) to handle duplicate node names const groups: Record = {}; storage.forEach((s) => { - if (!groups[s.node]) groups[s.node] = []; - groups[s.node].push(s); + const key = s.instance; // Use unique instance ID instead of hostname + if (!groups[key]) groups[key] = []; + groups[key].push(s); }); return groups; } else { @@ -553,34 +554,42 @@ const Storage: Component = () => { a.localeCompare(b))} + each={Object.entries(groupedStorage()).sort(([instanceIdA], [instanceIdB]) => { + // Sort by node name for display when in node mode + if (viewMode() === 'node') { + const nodeA = nodeByInstance()[instanceIdA]; + const nodeB = nodeByInstance()[instanceIdB]; + return (nodeA?.name || '').localeCompare(nodeB?.name || ''); + } + // Sort by storage name when in storage mode + return instanceIdA.localeCompare(instanceIdB); + })} > - {([groupName, storages]) => ( - <> - {/* Group Header */} - - - - - {groupName} - - - - + {([groupKey, storages]) => { + const node = viewMode() === 'node' ? nodeByInstance()[groupKey] : null; + return ( + <> + {/* Group Header */} + + {(validNode) => ( + + + + {validNode().name} + + + + )} + {/* Storage Rows */} }> @@ -817,7 +826,8 @@ const Storage: Component = () => { }} - )} + ); + }}