diff --git a/frontend-modern/src/components/Alerts/ThresholdsTable.tsx b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx new file mode 100644 index 000000000..5249280e1 --- /dev/null +++ b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx @@ -0,0 +1,721 @@ +import { createSignal, createMemo, For, Show } from 'solid-js'; +import type { VM, Container, Node } from '@/types/api'; + +interface Override { + id: string; + name: string; + type: 'guest' | 'node'; + resourceType?: string; + vmid?: number; + node?: string; + instance?: string; + disabled?: boolean; + thresholds: { + cpu?: number; + memory?: number; + disk?: number; + diskRead?: number; + diskWrite?: number; + networkIn?: number; + networkOut?: number; + }; +} + +// Simple threshold object for the UI +interface SimpleThresholds { + cpu?: number; + memory?: number; + disk?: number; + diskRead?: number; + diskWrite?: number; + networkIn?: number; + networkOut?: number; + [key: string]: number | undefined; // Add index signature for compatibility +} + +interface ThresholdsTableProps { + overrides: () => Override[]; + setOverrides: (overrides: Override[]) => void; + rawOverridesConfig: () => Record; + setRawOverridesConfig: (config: Record) => void; + allGuests: () => (VM | Container)[]; + nodes: Node[]; + guestDefaults: SimpleThresholds; + setGuestDefaults: (value: Record | ((prev: Record) => Record)) => void; + nodeDefaults: SimpleThresholds; + setNodeDefaults: (value: Record | ((prev: Record) => Record)) => void; + storageDefault: () => number; + setStorageDefault: (value: number) => void; + timeThreshold: () => number; + setTimeThreshold: (value: number) => void; + setHasUnsavedChanges: (value: boolean) => void; +} + +export function ThresholdsTable(props: ThresholdsTableProps) { + const [searchTerm, setSearchTerm] = createSignal(''); + const [showGlobalSettings, setShowGlobalSettings] = createSignal(true); + const [editingId, setEditingId] = createSignal(null); + const [editingThresholds, setEditingThresholds] = createSignal>({}); + + // Combine all resources (guests and nodes) with their overrides + const resourcesWithOverrides = createMemo(() => { + const search = searchTerm().toLowerCase(); + const overridesMap = new Map(props.overrides().map(o => [o.id, o])); + + // Process guests + const guests = props.allGuests().map(guest => { + const guestId = guest.id || `${guest.instance}-${guest.name}-${guest.vmid}`; + const override = overridesMap.get(guestId); + + return { + id: guestId, + name: guest.name, + type: 'guest' as const, + resourceType: guest.type === 'qemu' ? 'VM' : 'CT', + vmid: guest.vmid, + node: guest.node, + instance: guest.instance, + status: guest.status, + hasOverride: !!override, + disabled: override?.disabled || false, + thresholds: override?.thresholds || props.guestDefaults + }; + }); + + // Process nodes + const nodes = props.nodes.map(node => { + const override = overridesMap.get(node.id); + + return { + id: node.id, + name: node.name, + type: 'node' as const, + resourceType: 'Node', + status: node.status, + hasOverride: !!override, + disabled: false, + thresholds: override?.thresholds || props.nodeDefaults + }; + }); + + // Combine and filter + const allResources = [...guests, ...nodes]; + + if (search) { + return allResources.filter(r => + r.name.toLowerCase().includes(search) || + ('vmid' in r && r.vmid && r.vmid.toString().includes(search)) || + ('node' in r && r.node && r.node.toLowerCase().includes(search)) + ); + } + + return allResources; + }); + + // Group resources by node for display + const groupedResources = createMemo(() => { + const groups: Record = {}; + + resourcesWithOverrides().forEach(resource => { + const groupKey = resource.type === 'node' ? 'Nodes' : ('node' in resource ? resource.node : 'Unknown'); + + if (!groups[groupKey]) { + groups[groupKey] = []; + } + groups[groupKey].push(resource); + }); + + // Sort resources within each group + Object.keys(groups).forEach(key => { + groups[key] = groups[key].sort((a, b) => { + if (a.type === 'node' && b.type !== 'node') return -1; + if (a.type !== 'node' && b.type === 'node') return 1; + if ('vmid' in a && 'vmid' in b && a.vmid && b.vmid) return a.vmid - b.vmid; + return a.name.localeCompare(b.name); + }); + }); + + return groups; + }); + + const startEditing = (resourceId: string, currentThresholds: any) => { + setEditingId(resourceId); + setEditingThresholds(currentThresholds); + }; + + const saveEdit = (resourceId: string) => { + const resource = resourcesWithOverrides().find(r => r.id === resourceId); + if (!resource) return; + + const thresholds = editingThresholds(); + + // Create or update override + const override: Override = { + id: resourceId, + name: resource.name, + type: resource.type, + resourceType: resource.resourceType, + vmid: 'vmid' in resource ? resource.vmid : undefined, + node: 'node' in resource ? resource.node : undefined, + instance: 'instance' in resource ? resource.instance : undefined, + disabled: resource.disabled, + thresholds + }; + + // Update overrides list + const existingIndex = props.overrides().findIndex(o => o.id === resourceId); + if (existingIndex >= 0) { + const newOverrides = [...props.overrides()]; + newOverrides[existingIndex] = override; + props.setOverrides(newOverrides); + } else { + props.setOverrides([...props.overrides(), override]); + } + + // Update raw config + const newRawConfig = { ...props.rawOverridesConfig() }; + const hysteresisThresholds: Record = {}; + Object.entries(thresholds).forEach(([metric, value]) => { + if (value !== undefined && value !== null) { + hysteresisThresholds[metric] = { + trigger: value, + clear: Math.max(0, (value as number) - 5) + }; + } + }); + if (resource.disabled) { + hysteresisThresholds.disabled = true; + } + newRawConfig[resourceId] = hysteresisThresholds; + props.setRawOverridesConfig(newRawConfig); + + props.setHasUnsavedChanges(true); + setEditingId(null); + setEditingThresholds({}); + }; + + const cancelEdit = () => { + setEditingId(null); + setEditingThresholds({}); + }; + + const removeOverride = (resourceId: string) => { + props.setOverrides(props.overrides().filter(o => o.id !== resourceId)); + + const newRawConfig = { ...props.rawOverridesConfig() }; + delete newRawConfig[resourceId]; + props.setRawOverridesConfig(newRawConfig); + + props.setHasUnsavedChanges(true); + }; + + const toggleDisabled = (resourceId: string) => { + const resource = resourcesWithOverrides().find(r => r.id === resourceId); + if (!resource || resource.type !== 'guest') return; + + const override: Override = { + id: resourceId, + name: resource.name, + type: resource.type, + resourceType: resource.resourceType, + vmid: 'vmid' in resource ? resource.vmid : undefined, + node: 'node' in resource ? resource.node : undefined, + instance: 'instance' in resource ? resource.instance : undefined, + disabled: !resource.disabled, + thresholds: resource.thresholds + }; + + const existingIndex = props.overrides().findIndex(o => o.id === resourceId); + if (existingIndex >= 0) { + const newOverrides = [...props.overrides()]; + newOverrides[existingIndex] = override; + props.setOverrides(newOverrides); + } else { + props.setOverrides([...props.overrides(), override]); + } + + // Update raw config + const newRawConfig = { ...props.rawOverridesConfig() }; + const hysteresisThresholds: Record = {}; + Object.entries(resource.thresholds).forEach(([metric, value]) => { + if (value !== undefined && value !== null) { + hysteresisThresholds[metric] = { + trigger: value, + clear: Math.max(0, (value as number) - 5) + }; + } + }); + hysteresisThresholds.disabled = !resource.disabled; + newRawConfig[resourceId] = hysteresisThresholds; + props.setRawOverridesConfig(newRawConfig); + + props.setHasUnsavedChanges(true); + }; + + return ( +
+ {/* Global Settings Section */} +
+
setShowGlobalSettings(!showGlobalSettings())} + > +
+

Global Default Thresholds

+

+ Default thresholds that apply to all resources unless overridden +

+
+ + + +
+ + +
+ {/* Time Threshold */} +
+ + { + props.setTimeThreshold(parseInt(e.currentTarget.value) || 0); + props.setHasUnsavedChanges(true); + }} + class="w-20 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 + focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> + + seconds {props.timeThreshold() === 0 ? '(disabled)' : ''} + +
+ + {/* Default Thresholds Grid */} +
+ {/* Guest Defaults */} +
+

VMs & Containers

+
+
+ + { + props.setGuestDefaults((prev) => ({...prev, cpu: parseInt(e.currentTarget.value) || 0})); + props.setHasUnsavedChanges(true); + }} + class="w-16 px-2 py-0.5 text-sm border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" + /> + % +
+
+ + { + props.setGuestDefaults((prev) => ({...prev, memory: parseInt(e.currentTarget.value) || 0})); + props.setHasUnsavedChanges(true); + }} + class="w-16 px-2 py-0.5 text-sm border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" + /> + % +
+
+ + { + props.setGuestDefaults((prev) => ({...prev, disk: parseInt(e.currentTarget.value) || 0})); + props.setHasUnsavedChanges(true); + }} + class="w-16 px-2 py-0.5 text-sm border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" + /> + % +
+
+
+ + {/* Node Defaults */} +
+

Proxmox Nodes

+
+
+ + { + props.setNodeDefaults((prev) => ({...prev, cpu: parseInt(e.currentTarget.value) || 0})); + props.setHasUnsavedChanges(true); + }} + class="w-16 px-2 py-0.5 text-sm border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" + /> + % +
+
+ + { + props.setNodeDefaults((prev) => ({...prev, memory: parseInt(e.currentTarget.value) || 0})); + props.setHasUnsavedChanges(true); + }} + class="w-16 px-2 py-0.5 text-sm border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" + /> + % +
+
+ + { + props.setNodeDefaults((prev) => ({...prev, disk: parseInt(e.currentTarget.value) || 0})); + props.setHasUnsavedChanges(true); + }} + class="w-16 px-2 py-0.5 text-sm border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" + /> + % +
+
+
+ + {/* Storage Default */} +
+

Storage

+
+
+ + { + props.setStorageDefault(parseInt(e.currentTarget.value) || 0); + props.setHasUnsavedChanges(true); + }} + class="w-16 px-2 py-0.5 text-sm border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" + /> + % +
+
+
+
+ +
+ +
+
+
+
+ + {/* Search Bar */} +
+ setSearchTerm(e.currentTarget.value)} + class="w-full px-4 py-2 pl-10 text-sm border border-gray-300 dark:border-gray-600 rounded-lg + bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 + focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> + + + +
+ + {/* Resources Table */} +
+
+ + + + + + + + + + + + + + + { + if (a === 'Nodes') return -1; + if (b === 'Nodes') return 1; + return a.localeCompare(b); + })}> + {([groupName, resources]) => ( + <> + {/* Group header */} + + + + {/* Resource rows */} + + {(resource) => { + const isEditing = () => editingId() === resource.id; + const thresholds = () => isEditing() ? editingThresholds() : resource.thresholds; + + return ( + + + + + + + + + + + ); + }} + + + )} + + }> + + + + + +
+ Resource + + Type + + Status + + CPU % + + Memory % + + Disk % + + Alerts + + Actions +
+ {groupName} +
+
+ + {resource.name} + + + ({resource.vmid}) + + + + Custom + + +
+
+ + {resource.resourceType} + + + + {resource.status} + + + + {thresholds().cpu || '-'} + + }> + 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" + /> + + + + {thresholds().memory || '-'} + + }> + 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" + /> + + + + {thresholds().disk || '-'} + + }> + 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" + /> + + + + + + + - + + +
+ + + + + + + }> + + + +
+
+ No resources found +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend-modern/src/pages/Alerts.tsx b/frontend-modern/src/pages/Alerts.tsx index 458154ae8..9ab20b6ff 100644 --- a/frontend-modern/src/pages/Alerts.tsx +++ b/frontend-modern/src/pages/Alerts.tsx @@ -1,15 +1,15 @@ import { createSignal, Show, For, createMemo, createEffect, onMount } from 'solid-js'; -import { ThresholdSlider } from '@/components/Dashboard/ThresholdSlider'; import { EmailProviderSelect } from '@/components/Alerts/EmailProviderSelect'; import { WebhookConfig } from '@/components/Alerts/WebhookConfig'; import { CustomRulesTab } from '@/components/Alerts/CustomRulesTab'; +import { ThresholdsTable } from '@/components/Alerts/ThresholdsTable'; import { useWebSocket } from '@/App'; import { showSuccess, showError } from '@/utils/toast'; import { AlertsAPI } from '@/api/alerts'; import { NotificationsAPI, Webhook } from '@/api/notifications'; import type { EmailConfig } from '@/api/notifications'; import type { HysteresisThreshold, AlertThresholds } from '@/types/alerts'; -import type { Alert, State } from '@/types/api'; +import type { Alert, State, VM, Container } from '@/types/api'; type AlertTab = 'overview' | 'thresholds' | 'destinations' | 'schedule' | 'history' | 'custom-rules'; @@ -313,15 +313,7 @@ export function Alerts() { const allGuests = createMemo(() => { const vms = state.vms || []; const containers = state.containers || []; - return [...vms, ...containers].map(g => ({ - id: g.id, - name: g.name, - vmid: g.vmid, - type: g.type, // Keep original type (qemu/lxc) - displayType: g.type === 'qemu' ? 'VM' : 'CT', // Add display type - node: g.node, - instance: g.instance - })); + return [...vms, ...containers]; }, [], { equals: (prev, next) => { // Only update if the actual guest list changed if (prev.length !== next.length) return false; @@ -579,9 +571,9 @@ export function Alerts() { setRawOverridesConfig={setRawOverridesConfig} allGuests={allGuests} state={state} - guestDefaults={guestDefaults()} + guestDefaults={guestDefaults} setGuestDefaults={setGuestDefaults} - nodeDefaults={nodeDefaults()} + nodeDefaults={nodeDefaults} setNodeDefaults={setNodeDefaults} storageDefault={storageDefault} setStorageDefault={setStorageDefault} @@ -818,411 +810,13 @@ function OverviewTab(props: { ); } -// Add Override Form Component -function AddOverrideForm(props: { - guests: Array<{ id: string; name: string; vmid: number; type: string; node: string; instance: string }>; - nodes: Array<{ id: string; name: string }>; - existingOverrides: Override[]; - editingOverride?: Override | null; - onAdd: (override: Override) => void; - onUpdate?: (override: Override) => void; - onCancelEdit?: () => void; -}) { - const [resourceType, setResourceType] = createSignal<'guest' | 'node'>('guest'); - const [selectedResource, setSelectedResource] = createSignal(''); - const [alertsDisabled, setAlertsDisabled] = createSignal(false); - const [thresholds, setThresholds] = createSignal({ - cpu: 80, - memory: 85, - disk: 90 - }); - const [enabledThresholds, setEnabledThresholds] = createSignal({ - cpu: true, - memory: true, - disk: true - }); - - // When editing, populate the form with existing values - let previousEditingId: string | null = null; - createEffect(() => { - const currentEditing = props.editingOverride; - const currentId = currentEditing?.id || null; - - // Only update if the editing override changed - if (currentId !== previousEditingId) { - previousEditingId = currentId; - - if (currentEditing) { - setResourceType(currentEditing.type); - setSelectedResource(currentEditing.id); - setAlertsDisabled(currentEditing.disabled || false); - - const editThresholds = { cpu: 80, memory: 85, disk: 90 }; - const editEnabled = { cpu: false, memory: false, disk: false }; - - Object.entries(currentEditing.thresholds).forEach(([key, value]) => { - if (key in editThresholds) { - editThresholds[key as keyof typeof editThresholds] = value as number; - editEnabled[key as keyof typeof editEnabled] = true; - } - }); - - setThresholds(editThresholds); - setEnabledThresholds(editEnabled); - } else { - // Reset when not editing - setResourceType('guest'); - setSelectedResource(''); - setAlertsDisabled(false); - setThresholds({ cpu: 80, memory: 85, disk: 90 }); - setEnabledThresholds({ cpu: true, memory: true, disk: true }); - } - } - }); - - const availableResources = createMemo(() => { - const existingIds = new Set(props.existingOverrides.map(o => o.id)); - - if (resourceType() === 'node') { - return props.nodes.filter(n => !existingIds.has(n.id)); - } else { - return props.guests.filter(g => !existingIds.has(g.id)); - } - }); - - const handleSubmit = () => { - const resourceId = selectedResource(); - if (!resourceId) return; - - const activeThresholds: Record = {}; - const thresholdValues = thresholds(); - Object.entries(enabledThresholds()).forEach(([key, enabled]) => { - if (enabled && key in thresholdValues) { - activeThresholds[key] = thresholdValues[key as keyof typeof thresholdValues]; - } - }); - - // Allow creation if either thresholds are set OR alerts are disabled - if (Object.keys(activeThresholds).length === 0 && !alertsDisabled()) return; - - let override: Override; - - if (props.editingOverride) { - // Update existing override - override = { - ...props.editingOverride, - disabled: alertsDisabled(), - thresholds: activeThresholds - }; - props.onUpdate?.(override); - } else { - // Create new override - if (resourceType() === 'node') { - const node = props.nodes.find(n => n.id === resourceId); - if (!node) return; - - override = { - id: node.id, - name: node.name, - type: 'node', - resourceType: 'Node', - thresholds: activeThresholds - }; - } else { - const guest = props.guests.find(g => g.id === resourceId); - if (!guest) return; - - override = { - id: guest.id, - name: guest.name, - type: 'guest', - resourceType: guest.type === 'qemu' ? 'VM' : 'CT', - vmid: guest.vmid, - node: guest.node, - instance: guest.instance, - disabled: alertsDisabled(), - thresholds: activeThresholds - }; - } - props.onAdd(override); - } - - // Reset form after successful submission - setSelectedResource(''); - setAlertsDisabled(false); - setThresholds({ cpu: 80, memory: 85, disk: 90 }); - setEnabledThresholds({ cpu: true, memory: true, disk: true }); - }; - - return ( -
-
-

- - - - {props.editingOverride ? 'Edit Override' : 'Add New Override'} -

- - - -
- -
- {/* Resource Selection */} -
-
- - - - {/* Resource Type Toggle */} -
- - -
- - - - }> - {/* When editing, just show the resource name */} -
- - {props.editingOverride?.name} - {props.editingOverride?.vmid && ` (${props.editingOverride.vmid})`} - - {props.editingOverride?.type === 'node' ? 'Node' : props.editingOverride?.resourceType} - - -
-
-
-
- - {/* Threshold Configuration */} -
- - - {/* Alert Mode Selection for Guests */} - -
- - -
-
- -
- - {(metric) => ( -
-
- - {thresholds()[metric as keyof typeof thresholds]}% -
- { - setThresholds({ - ...thresholds(), - [metric]: value - }); - // Auto-enable this threshold when value changes - setEnabledThresholds({ - ...enabledThresholds(), - [metric]: true - }); - }} - type={metric as 'cpu' | 'memory' | 'disk'} - /> -
- )} -
-
-
-
- -
- -
-
- ); -} - -// Override Item Component -function OverrideItem(props: { - override: Override; - onEdit: () => void; - onRemove: () => void; -}) { - - return ( -
-
-
-
-

- {props.override.name} - - ({props.override.vmid}) - -

- - {props.override.type === 'node' ? 'Node' : props.override.resourceType} - - - - Alerts Disabled - - -
- - - All alerts disabled for this guest -
- }> -
- v)}> - {([key, value]) => ( -
- {key.toUpperCase()}: - {value as number}% -
- )} -
-
- -
- -
- - -
-
- - ); -} // Thresholds Tab - Improved design interface ThresholdsTabProps { - allGuests: () => Array<{ id: string; name: string; vmid: number; type: string; node: string; instance: string }>; + allGuests: () => (VM | Container)[]; state: State; - guestDefaults: Record; - nodeDefaults: Record; + guestDefaults: () => Record; + nodeDefaults: () => Record; storageDefault: () => number; timeThreshold: () => number; overrides: () => Override[]; @@ -1238,318 +832,25 @@ interface ThresholdsTabProps { } function ThresholdsTab(props: ThresholdsTabProps) { - // Track which override is currently being edited - const [editingOverride, setEditingOverride] = createSignal(null); - + // Use the new table component for a cleaner, more information-dense layout return ( -
- {/* Time Threshold */} -
-

Time Threshold

-

- Require thresholds to be exceeded for a minimum duration before triggering alerts (helps prevent false positives from brief spikes) -

-
- - { - props.setTimeThreshold(parseInt(e.currentTarget.value) || 0); - props.setHasUnsavedChanges(true); - }} - class="w-24 px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg - bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 - focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 dark:focus:border-blue-400" - /> - - {props.timeThreshold() === 0 ? '(disabled)' : `Wait ${props.timeThreshold()}s before alerting`} - -
-
- - {/* Step 1: Global Default Thresholds */} -
-
-
-

Default Alert Thresholds

-

- These thresholds apply to all resources unless overridden below -

-
- -
- -
- {/* VMs & Containers */} -
-

- - - - - - - VMs & Containers -

- -
-
-
- - {props.guestDefaults.cpu}% -
- { - props.setGuestDefaults((prev) => ({...prev, cpu: v})); - props.setHasUnsavedChanges(true); - }} - type="cpu" - /> -
- -
-
- - {props.guestDefaults.memory}% -
- { - props.setGuestDefaults((prev) => ({...prev, memory: v})); - props.setHasUnsavedChanges(true); - }} - type="memory" - /> -
- -
-
- - {props.guestDefaults.disk}% -
- { - props.setGuestDefaults((prev) => ({...prev, disk: v})); - props.setHasUnsavedChanges(true); - }} - type="disk" - /> -
-
-
- - {/* Proxmox Nodes */} -
-

- - - - Proxmox Nodes -

- -
-
-
- - {props.nodeDefaults.cpu}% -
- { - props.setNodeDefaults((prev) => ({...prev, cpu: v})); - props.setHasUnsavedChanges(true); - }} - type="cpu" - /> -
- -
-
- - {props.nodeDefaults.memory}% -
- { - props.setNodeDefaults((prev) => ({...prev, memory: v})); - props.setHasUnsavedChanges(true); - }} - type="memory" - /> -
- -
-
- - {props.nodeDefaults.disk}% -
- { - props.setNodeDefaults((prev) => ({...prev, disk: v})); - props.setHasUnsavedChanges(true); - }} - type="disk" - /> -
-
-
- - {/* Storage */} -
-

- - - - - - Storage -

- -
-
-
- - {props.storageDefault()}% -
- { - props.setStorageDefault(v); - props.setHasUnsavedChanges(true); - }} - type="disk" - /> -
-
-
-
-
- - {/* Step 2: Resource-Specific Overrides */} -
-
-

Custom Overrides

-

- Override default thresholds for specific VMs, containers, or nodes -

-
- - {/* Existing Overrides List */} - 0}> -
-

- Active Overrides ({props.overrides().length}) -

-
- - {(override) => ( - setEditingOverride(override)} - onRemove={() => { - props.setOverrides(props.overrides().filter((o) => o.id !== override.id)); - // Update raw config too - const newRawConfig = { ...props.rawOverridesConfig() }; - delete newRawConfig[override.id]; - props.setRawOverridesConfig(newRawConfig); - props.setHasUnsavedChanges(true); - }} - /> - )} - -
-
-
- - {/* Add New Override Form */} -
-
-
- - - - -
-
-

Add Custom Override

-

Set different thresholds for a specific resource

-
-
- - { - props.setOverrides([...props.overrides(), override]); - // Update raw config too - const newRawConfig = { ...props.rawOverridesConfig() }; - const hysteresisThresholds: Record = {}; - Object.entries(override.thresholds).forEach(([metric, value]) => { - hysteresisThresholds[metric] = { trigger: value, clear: Math.max(0, (value as number) - 5) }; - }); - // Include disabled field if present - if (override.disabled) { - hysteresisThresholds.disabled = true; - } - newRawConfig[override.id] = hysteresisThresholds; - props.setRawOverridesConfig(newRawConfig); - props.setHasUnsavedChanges(true); - }} - onUpdate={(override) => { - props.setOverrides(props.overrides().map((o: Override) => - o.id === override.id ? override : o - )); - // Update raw config too - const newRawConfig = { ...props.rawOverridesConfig() }; - const hysteresisThresholds: Record = {}; - Object.entries(override.thresholds).forEach(([metric, value]) => { - hysteresisThresholds[metric] = { trigger: value, clear: Math.max(0, (value as number) - 5) }; - }); - // Include disabled field if present - if (override.disabled) { - hysteresisThresholds.disabled = true; - } - newRawConfig[override.id] = hysteresisThresholds; - props.setRawOverridesConfig(newRawConfig); - props.setHasUnsavedChanges(true); - setEditingOverride(null); - }} - onCancelEdit={() => setEditingOverride(null)} - /> -
-
-
+ ); }