diff --git a/frontend-modern/src/components/Alerts/ResourceTable.tsx b/frontend-modern/src/components/Alerts/ResourceTable.tsx index 06e9b30c0..2d091e426 100644 --- a/frontend-modern/src/components/Alerts/ResourceTable.tsx +++ b/frontend-modern/src/components/Alerts/ResourceTable.tsx @@ -3,7 +3,8 @@ import type { Alert } from '@/types/api'; interface ResourceTableProps { title: string; - resources: any[]; + resources?: any[]; + groupedResources?: Record; columns: string[]; activeAlerts?: Record; onEdit: (resourceId: string, thresholds: any, defaults: any) => void; @@ -77,9 +78,217 @@ export function ResourceTable(props: ResourceTableProps) { - - {(resource) => { + + a.localeCompare(b))}> + {([nodeName, resources]) => ( + <> + {/* Node group header */} + + + {nodeName} + + + {/* Resources in this group */} + + {(resource) => { + const isEditing = () => props.editingId() === resource.id; + const thresholds = () => isEditing() ? props.editingThresholds() : resource.thresholds; + const displayValue = (metric: string) => { + if (isEditing()) return thresholds()[metric] || resource.defaults[metric] || ''; + return resource.thresholds[metric] || resource.defaults[metric] || 0; + }; + const isOverridden = (metric: string) => { + return resource.thresholds[metric] !== undefined && resource.thresholds[metric] !== null; + }; + + return ( + + +
+ + {resource.name} + + + ({resource.vmid}) + + + + Custom + + +
+ + + + {resource.resourceType} + + + + + {resource.status} + + + + {/* Metric columns - dynamically rendered based on resource type */} + + {(column) => { + const metric = column.toLowerCase().replace(' %', '').replace(' mb/s', '').replace('disk r', 'diskRead').replace('disk w', 'diskWrite').replace('net in', 'networkIn').replace('net out', 'networkOut'); + + // Check if this metric applies to this resource type + const showMetric = () => { + if (resource.type === 'node' && ['diskRead', 'diskWrite', 'networkIn', 'networkOut'].includes(metric)) { + return false; + } + if (resource.type === 'storage') { + return metric === 'usage'; + } + return true; + }; + + return ( + + - + }> + + }> + props.setEditingThresholds({ + ...props.editingThresholds(), + [metric]: parseInt(e.currentTarget.value) || undefined + })} + class="w-14 px-1 py-0.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" + /> + + + + ); + }} + + + {/* Alerts column */} + + + + + + + + + + + + + {/* Actions column */} + +
+ + + + + }> + + + + + +
+ + + ); + }} +
+ + )} +
+
+ + + {(resource) => { const isEditing = () => props.editingId() === resource.id; const thresholds = () => isEditing() ? props.editingThresholds() : resource.thresholds; const displayValue = (metric: string) => { @@ -276,12 +485,13 @@ export function ResourceTable(props: ResourceTableProps) { ); }} - }> - - - No {props.title.toLowerCase()} found - - + }> + + + No {props.title.toLowerCase()} found + + + diff --git a/frontend-modern/src/components/Alerts/ThresholdsTable.tsx b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx index eb62f9ed7..f5aad2a18 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTable.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx @@ -163,8 +163,8 @@ export function ThresholdsTable(props: ThresholdsTableProps) { return nodes; }); - // Process guests with their overrides - const guestsWithOverrides = createMemo(() => { + // Process guests with their overrides and group by node + const guestsGroupedByNode = createMemo(() => { const search = searchTerm().toLowerCase(); const overridesMap = new Map(props.overrides().map(o => [o.id, o])); @@ -196,14 +196,33 @@ export function ThresholdsTable(props: ThresholdsTableProps) { }; }); - if (search) { - return guests.filter(g => - g.name.toLowerCase().includes(search) || - g.vmid?.toString().includes(search) || - g.node?.toLowerCase().includes(search) - ); - } - return guests; + const filteredGuests = search + ? guests.filter(g => + g.name.toLowerCase().includes(search) || + g.vmid?.toString().includes(search) || + g.node?.toLowerCase().includes(search) + ) + : guests; + + // Group by node + const grouped: Record = {}; + filteredGuests.forEach(guest => { + const node = guest.node || 'Unknown'; + if (!grouped[node]) { + grouped[node] = []; + } + grouped[node].push(guest); + }); + + // Sort guests within each group by vmid + Object.keys(grouped).forEach(node => { + grouped[node].sort((a, b) => { + if (a.vmid && b.vmid) return a.vmid - b.vmid; + return a.name.localeCompare(b.name); + }); + }); + + return grouped; }); // Process storage with their overrides @@ -251,7 +270,9 @@ export function ThresholdsTable(props: ThresholdsTableProps) { }; const saveEdit = (resourceId: string) => { - const allResources = [...nodesWithOverrides(), ...guestsWithOverrides(), ...storageWithOverrides()]; + // Flatten grouped guests to find the resource + const allGuests = Object.values(guestsGroupedByNode()).flat(); + const allResources = [...nodesWithOverrides(), ...allGuests, ...storageWithOverrides()]; const resource = allResources.find(r => r.id === resourceId); if (!resource) return; @@ -346,7 +367,9 @@ export function ThresholdsTable(props: ThresholdsTableProps) { }; const toggleDisabled = (resourceId: string) => { - const allResources = [...guestsWithOverrides(), ...storageWithOverrides()]; + // Flatten grouped guests to find the resource + const allGuests = Object.values(guestsGroupedByNode()).flat(); + const allResources = [...allGuests, ...storageWithOverrides()]; const resource = allResources.find(r => r.id === resourceId); if (!resource || (resource.type !== 'guest' && resource.type !== 'storage')) return; @@ -835,10 +858,10 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
{/* Guests Table */} - 0}> + 0}>