From 88b972f8fd73880f71f602eded359233b390e3fd Mon Sep 17 00:00:00 2001 From: Pulse Monitor Date: Thu, 21 Aug 2025 11:01:10 +0000 Subject: [PATCH] feat: add storage device overrides to threshold table - Added Storage column to threshold table for individual storage thresholds - Storage devices now appear in their own "Storage" group - Each storage device can have its own usage threshold override - Storage shows "-" for CPU/Memory/Disk/IO metrics (not applicable) - Added orange badge styling for storage device type - Updated Override interface to support storage type and usage threshold - Fixed colspan values for new 13-column layout This allows users to set different thresholds for different storage pools (e.g. tighter monitoring for system storage vs bulk storage) --- .../src/components/Alerts/ThresholdsTable.tsx | 209 ++++++++++++------ frontend-modern/src/pages/Alerts.tsx | 10 +- 2 files changed, 150 insertions(+), 69 deletions(-) diff --git a/frontend-modern/src/components/Alerts/ThresholdsTable.tsx b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx index 2e3e6a139..6fff0a71f 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTable.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx @@ -1,10 +1,10 @@ import { createSignal, createMemo, For, Show, onMount, onCleanup } from 'solid-js'; -import type { VM, Container, Node, Alert } from '@/types/api'; +import type { VM, Container, Node, Alert, Storage } from '@/types/api'; interface Override { id: string; name: string; - type: 'guest' | 'node'; + type: 'guest' | 'node' | 'storage'; resourceType?: string; vmid?: number; node?: string; @@ -19,6 +19,7 @@ interface Override { diskWrite?: number; networkIn?: number; networkOut?: number; + usage?: number; // For storage devices }; } @@ -41,6 +42,7 @@ interface ThresholdsTableProps { setRawOverridesConfig: (config: Record) => void; allGuests: () => (VM | Container)[]; nodes: Node[]; + storage: Storage[]; guestDefaults: SimpleThresholds; setGuestDefaults: (value: Record | ((prev: Record) => Record)) => void; nodeDefaults: SimpleThresholds; @@ -105,7 +107,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) { if (value === undefined || value === null) return '0'; // Percentage-based metrics - if (metric === 'cpu' || metric === 'memory' || metric === 'disk') { + if (metric === 'cpu' || metric === 'memory' || metric === 'disk' || metric === 'usage') { return `${value}%`; } @@ -208,8 +210,31 @@ export function ThresholdsTable(props: ThresholdsTableProps) { }; }); + // Process storage devices + const storageDevices = props.storage.map(storage => { + const override = overridesMap.get(storage.id); + + // Storage only has usage threshold + const hasCustomThresholds = override?.thresholds?.usage !== undefined && + override.thresholds.usage !== props.storageDefault(); + + return { + id: storage.id, + name: storage.name, + type: 'storage' as const, + resourceType: 'Storage', + node: storage.node, + instance: storage.instance, + status: storage.status, + hasOverride: hasCustomThresholds || false, + disabled: override?.disabled || false, + thresholds: override?.thresholds || {}, + defaults: { usage: props.storageDefault() } + }; + }); + // Combine and filter - const allResources = [...guests, ...nodes]; + const allResources = [...guests, ...nodes, ...storageDevices]; if (search) { return allResources.filter(r => @@ -227,7 +252,14 @@ export function ThresholdsTable(props: ThresholdsTableProps) { const groups: Record = {}; resourcesWithOverrides().forEach(resource => { - const groupKey = resource.type === 'node' ? 'Nodes' : ('node' in resource ? resource.node : 'Unknown'); + let groupKey: string; + if (resource.type === 'node') { + groupKey = 'Nodes'; + } else if (resource.type === 'storage') { + groupKey = 'Storage'; + } else { + groupKey = 'node' in resource ? resource.node : 'Unknown'; + } if (!groups[groupKey]) { groups[groupKey] = []; @@ -293,7 +325,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) { const override: Override = { id: resourceId, name: resource.name, - type: resource.type, + type: resource.type as 'guest' | 'node' | 'storage', resourceType: resource.resourceType, vmid: 'vmid' in resource ? resource.vmid : undefined, node: 'node' in resource ? resource.node : undefined, @@ -841,6 +873,9 @@ export function ThresholdsTable(props: ThresholdsTableProps) { Disk % + + Storage % + Disk R
MB/s @@ -872,7 +907,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) { <> {/* Group header */} - + {groupName} @@ -893,6 +928,10 @@ export function ThresholdsTable(props: ThresholdsTableProps) { metric === 'networkIn' || metric === 'networkOut')) { return false; } + // Storage only has usage metric + if (resource.type === 'storage') { + return metric === 'usage'; + } return true; }; const isOverridden = (metric: string) => { @@ -925,6 +964,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) { @@ -943,72 +983,111 @@ export function ThresholdsTable(props: ThresholdsTableProps) { - + + }> + setEditingThresholds({ + ...editingThresholds(), + cpu: 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" + /> + }> - setEditingThresholds({ - ...editingThresholds(), - cpu: 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" - /> + - - + + }> + setEditingThresholds({ + ...editingThresholds(), + memory: 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" + /> + }> - setEditingThresholds({ - ...editingThresholds(), - memory: 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" - /> + - - + + }> + setEditingThresholds({ + ...editingThresholds(), + disk: 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" + /> + }> - setEditingThresholds({ - ...editingThresholds(), - disk: 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" - /> + - + + + + - + }> + + }> + setEditingThresholds({ + ...editingThresholds(), + usage: 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" + /> + @@ -1226,7 +1305,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) { }> - + No resources found diff --git a/frontend-modern/src/pages/Alerts.tsx b/frontend-modern/src/pages/Alerts.tsx index 468b71c76..9c2642208 100644 --- a/frontend-modern/src/pages/Alerts.tsx +++ b/frontend-modern/src/pages/Alerts.tsx @@ -50,12 +50,12 @@ interface ScheduleRef { interface Override { id: string; // Full ID (e.g. "Main-node1-105" for guest, "node-node1" for node) name: string; // Display name - type: 'guest' | 'node'; - resourceType?: string; // VM, CT, or Node + type: 'guest' | 'node' | 'storage'; + resourceType?: string; // VM, CT, Node, or Storage vmid?: number; // Only for guests - node?: string; // Node name (for guests), undefined for nodes themselves + node?: string; // Node name (for guests and storage), undefined for nodes themselves instance?: string; - disabled?: boolean; // Completely disable alerts for this guest + disabled?: boolean; // Completely disable alerts for this guest/storage disableConnectivity?: boolean; // For nodes - disable offline/connectivity alerts thresholds: { cpu?: number; @@ -65,6 +65,7 @@ interface Override { diskWrite?: number; networkIn?: number; networkOut?: number; + usage?: number; // For storage devices }; } @@ -849,6 +850,7 @@ function ThresholdsTab(props: ThresholdsTabProps) { setRawOverridesConfig={props.setRawOverridesConfig} allGuests={props.allGuests} nodes={props.state.nodes || []} + storage={props.state.storage || []} guestDefaults={props.guestDefaults()} setGuestDefaults={props.setGuestDefaults} nodeDefaults={props.nodeDefaults()}