mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-24 04:07:16 +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
|
// Create a mapping from node instance ID to node object
|
||||||
const nodeHostMap = createMemo(() => {
|
const nodeByInstance = createMemo(() => {
|
||||||
const map: Record<string, string> = {};
|
const map: Record<string, Node> = {};
|
||||||
props.nodes.forEach((node) => {
|
props.nodes.forEach((node) => {
|
||||||
if (node.host) {
|
map[node.id] = node;
|
||||||
map[node.name] = node.host;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return map;
|
return map;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Persist filter states to localStorage
|
// Persist filter states to localStorage
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
localStorage.setItem('dashboardViewMode', viewMode());
|
localStorage.setItem('dashboardViewMode', viewMode());
|
||||||
@@ -356,13 +355,14 @@ export function Dashboard(props: DashboardProps) {
|
|||||||
return groups;
|
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)[]> = {};
|
const groups: Record<string, (VM | Container)[]> = {};
|
||||||
guests.forEach((guest) => {
|
guests.forEach((guest) => {
|
||||||
if (!groups[guest.node]) {
|
const instanceId = guest.instance; // Use unique instance ID instead of hostname
|
||||||
groups[guest.node] = [];
|
if (!groups[instanceId]) {
|
||||||
|
groups[instanceId] = [];
|
||||||
}
|
}
|
||||||
groups[guest.node].push(guest);
|
groups[instanceId].push(guest);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort within each node group
|
// Sort within each node group
|
||||||
@@ -557,10 +557,10 @@ export function Dashboard(props: DashboardProps) {
|
|||||||
const diskPercent = () =>
|
const diskPercent = () =>
|
||||||
node.disk ? Math.round((node.disk.used / node.disk.total) * 100) : 0;
|
node.disk ? Math.round((node.disk.used / node.disk.total) * 100) : 0;
|
||||||
|
|
||||||
// Count VMs and containers for this node
|
// Count VMs and containers for this node (use instance ID to handle duplicate node names)
|
||||||
const nodeVMs = () => props.vms.filter((vm) => vm.node === node.name).length;
|
const nodeVMs = () => props.vms.filter((vm) => vm.instance === node.id).length;
|
||||||
const nodeContainers = () =>
|
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}`);
|
const isSelected = () => search().includes(`node:${node.name}`);
|
||||||
|
|
||||||
@@ -915,25 +915,29 @@ export function Dashboard(props: DashboardProps) {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<For
|
<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={<></>}
|
fallback={<></>}
|
||||||
>
|
>
|
||||||
{([node, guests]) => (
|
{([instanceId, guests]) => {
|
||||||
|
const node = nodeByInstance()[instanceId];
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
<Show when={node && groupingMode() === 'grouped'}>
|
<Show when={node && groupingMode() === 'grouped'}>
|
||||||
<tr class="bg-gray-50/50 dark:bg-gray-700/30">
|
<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]">
|
<td class="py-0.5 pl-6 pr-2 text-xs font-medium text-gray-600 dark:text-gray-400 w-[200px]">
|
||||||
<a
|
<a
|
||||||
href={
|
href={node.host || `https://${node.name}:8006`}
|
||||||
nodeHostMap()[node] ||
|
|
||||||
(node.includes(':') ? `https://${node}` : `https://${node}:8006`)
|
|
||||||
}
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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"
|
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>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td colspan="10" class="py-0.5 px-2"></td>
|
<td colspan="10" class="py-0.5 px-2"></td>
|
||||||
@@ -962,7 +966,8 @@ export function Dashboard(props: DashboardProps) {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</>
|
</>
|
||||||
)}
|
);
|
||||||
|
}}
|
||||||
</For>
|
</For>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -20,17 +20,16 @@ const Storage: Component = () => {
|
|||||||
const [sortKey, setSortKey] = createSignal<StorageSortKey>('name');
|
const [sortKey, setSortKey] = createSignal<StorageSortKey>('name');
|
||||||
const [sortDirection, setSortDirection] = createSignal<'asc' | 'desc'>('asc');
|
const [sortDirection, setSortDirection] = createSignal<'asc' | 'desc'>('asc');
|
||||||
|
|
||||||
// Create a mapping from node name to host URL
|
// Create a mapping from node instance ID to node object
|
||||||
const nodeHostMap = createMemo(() => {
|
const nodeByInstance = createMemo(() => {
|
||||||
const map: Record<string, string> = {};
|
const map: Record<string, typeof state.nodes[0]> = {};
|
||||||
(state.nodes || []).forEach((node) => {
|
(state.nodes || []).forEach((node) => {
|
||||||
if (node.host) {
|
map[node.id] = node;
|
||||||
map[node.name] = node.host;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return map;
|
return map;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const sortKeyOptions: { value: StorageSortKey; label: string }[] = [
|
const sortKeyOptions: { value: StorageSortKey; label: string }[] = [
|
||||||
{ value: 'name', label: 'Name' },
|
{ value: 'name', label: 'Name' },
|
||||||
{ value: 'node', label: 'Node' },
|
{ value: 'node', label: 'Node' },
|
||||||
@@ -243,10 +242,12 @@ const Storage: Component = () => {
|
|||||||
const mode = viewMode();
|
const mode = viewMode();
|
||||||
|
|
||||||
if (mode === 'node') {
|
if (mode === 'node') {
|
||||||
|
// Group by instance ID (not node name) to handle duplicate node names
|
||||||
const groups: Record<string, StorageType[]> = {};
|
const groups: Record<string, StorageType[]> = {};
|
||||||
storage.forEach((s) => {
|
storage.forEach((s) => {
|
||||||
if (!groups[s.node]) groups[s.node] = [];
|
const key = s.instance; // Use unique instance ID instead of hostname
|
||||||
groups[s.node].push(s);
|
if (!groups[key]) groups[key] = [];
|
||||||
|
groups[key].push(s);
|
||||||
});
|
});
|
||||||
return groups;
|
return groups;
|
||||||
} else {
|
} else {
|
||||||
@@ -553,34 +554,42 @@ const Storage: Component = () => {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<For
|
<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]) => (
|
{([groupKey, storages]) => {
|
||||||
<>
|
const node = viewMode() === 'node' ? nodeByInstance()[groupKey] : null;
|
||||||
{/* Group Header */}
|
return (
|
||||||
<Show when={viewMode() === 'node'}>
|
<>
|
||||||
<tr class="bg-gray-50/50 dark:bg-gray-700/30">
|
{/* Group Header */}
|
||||||
<td
|
<Show when={viewMode() === 'node' && node}>
|
||||||
class="p-0.5 px-1.5 text-xs font-medium text-gray-600 dark:text-gray-400"
|
{(validNode) => (
|
||||||
colspan="9"
|
<tr class="bg-gray-50/50 dark:bg-gray-700/30">
|
||||||
>
|
<td
|
||||||
<a
|
class="p-0.5 px-1.5 text-xs font-medium text-gray-600 dark:text-gray-400"
|
||||||
href={
|
colspan="9"
|
||||||
nodeHostMap()[groupName] ||
|
>
|
||||||
(groupName.includes(':')
|
<a
|
||||||
? `https://${groupName}`
|
href={validNode().host || `https://${validNode().name}:8006`}
|
||||||
: `https://${groupName}:8006`)
|
target="_blank"
|
||||||
}
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
class="text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-150 cursor-pointer"
|
||||||
rel="noopener noreferrer"
|
title={`Open ${validNode().name} web interface`}
|
||||||
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`}
|
{validNode().name}
|
||||||
>
|
</a>
|
||||||
{groupName}
|
</td>
|
||||||
</a>
|
</tr>
|
||||||
</td>
|
)}
|
||||||
</tr>
|
</Show>
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* Storage Rows */}
|
{/* Storage Rows */}
|
||||||
<For each={storages} fallback={<></>}>
|
<For each={storages} fallback={<></>}>
|
||||||
@@ -817,7 +826,8 @@ const Storage: Component = () => {
|
|||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
</>
|
</>
|
||||||
)}
|
);
|
||||||
|
}}
|
||||||
</For>
|
</For>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
Reference in New Issue
Block a user