mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-23 03:33:53 +00:00
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.
This commit is contained in:
@@ -98,17 +98,16 @@ export function Dashboard(props: DashboardProps) {
|
||||
}
|
||||
});
|
||||
|
||||
// Create a mapping from node name to host URL
|
||||
const nodeHostMap = createMemo(() => {
|
||||
const map: Record<string, string> = {};
|
||||
// Create a mapping from node instance ID to node object
|
||||
const nodeByInstance = createMemo(() => {
|
||||
const map: Record<string, Node> = {};
|
||||
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<string, (VM | Container)[]> = {};
|
||||
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) {
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<For
|
||||
each={Object.entries(groupedGuests()).sort(([a], [b]) => 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 (
|
||||
<>
|
||||
<Show when={node && groupingMode() === 'grouped'}>
|
||||
<tr class="bg-gray-50/50 dark:bg-gray-700/30">
|
||||
<td class="py-0.5 pl-6 pr-2 text-xs font-medium text-gray-600 dark:text-gray-400 w-[200px]">
|
||||
<a
|
||||
href={
|
||||
nodeHostMap()[node] ||
|
||||
(node.includes(':') ? `https://${node}` : `https://${node}:8006`)
|
||||
}
|
||||
href={node.host || `https://${node.name}:8006`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-150 cursor-pointer"
|
||||
title={`Open ${node} web interface`}
|
||||
title={`Open ${node.name} web interface`}
|
||||
>
|
||||
{node}
|
||||
{node.name}
|
||||
</a>
|
||||
</td>
|
||||
<td colspan="10" class="py-0.5 px-2"></td>
|
||||
@@ -962,7 +966,8 @@ export function Dashboard(props: DashboardProps) {
|
||||
)}
|
||||
</For>
|
||||
</>
|
||||
)}
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -20,17 +20,16 @@ const Storage: Component = () => {
|
||||
const [sortKey, setSortKey] = createSignal<StorageSortKey>('name');
|
||||
const [sortDirection, setSortDirection] = createSignal<'asc' | 'desc'>('asc');
|
||||
|
||||
// Create a mapping from node name to host URL
|
||||
const nodeHostMap = createMemo(() => {
|
||||
const map: Record<string, string> = {};
|
||||
// Create a mapping from node instance ID to node object
|
||||
const nodeByInstance = createMemo(() => {
|
||||
const map: Record<string, typeof state.nodes[0]> = {};
|
||||
(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<string, StorageType[]> = {};
|
||||
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 = () => {
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<For
|
||||
each={Object.entries(groupedStorage()).sort(([a], [b]) => 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 */}
|
||||
<Show when={viewMode() === 'node'}>
|
||||
<tr class="bg-gray-50/50 dark:bg-gray-700/30">
|
||||
<td
|
||||
class="p-0.5 px-1.5 text-xs font-medium text-gray-600 dark:text-gray-400"
|
||||
colspan="9"
|
||||
>
|
||||
<a
|
||||
href={
|
||||
nodeHostMap()[groupName] ||
|
||||
(groupName.includes(':')
|
||||
? `https://${groupName}`
|
||||
: `https://${groupName}:8006`)
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-150 cursor-pointer"
|
||||
title={`Open ${groupName} web interface`}
|
||||
>
|
||||
{groupName}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</Show>
|
||||
{([groupKey, storages]) => {
|
||||
const node = viewMode() === 'node' ? nodeByInstance()[groupKey] : null;
|
||||
return (
|
||||
<>
|
||||
{/* Group Header */}
|
||||
<Show when={viewMode() === 'node' && node}>
|
||||
{(validNode) => (
|
||||
<tr class="bg-gray-50/50 dark:bg-gray-700/30">
|
||||
<td
|
||||
class="p-0.5 px-1.5 text-xs font-medium text-gray-600 dark:text-gray-400"
|
||||
colspan="9"
|
||||
>
|
||||
<a
|
||||
href={validNode().host || `https://${validNode().name}:8006`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-150 cursor-pointer"
|
||||
title={`Open ${validNode().name} web interface`}
|
||||
>
|
||||
{validNode().name}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
{/* Storage Rows */}
|
||||
<For each={storages} fallback={<></>}>
|
||||
@@ -817,7 +826,8 @@ const Storage: Component = () => {
|
||||
}}
|
||||
</For>
|
||||
</>
|
||||
)}
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user