diff --git a/frontend-modern/src/components/Alerts/ThresholdsTable.tsx b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx index ef1d8b586..507af6956 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTable.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx @@ -1,5 +1,5 @@ -import { createSignal, createMemo, For, Show } from 'solid-js'; -import type { VM, Container, Node } from '@/types/api'; +import { createSignal, createMemo, For, Show, onMount, onCleanup } from 'solid-js'; +import type { VM, Container, Node, Alert } from '@/types/api'; interface Override { id: string; @@ -10,6 +10,7 @@ interface Override { node?: string; instance?: string; disabled?: boolean; + disableConnectivity?: boolean; // For nodes only - disable offline alerts thresholds: { cpu?: number; memory?: number; @@ -49,6 +50,7 @@ interface ThresholdsTableProps { timeThreshold: () => number; setTimeThreshold: (value: number) => void; setHasUnsavedChanges: (value: boolean) => void; + activeAlerts?: Record; } export function ThresholdsTable(props: ThresholdsTableProps) { @@ -57,6 +59,95 @@ export function ThresholdsTable(props: ThresholdsTableProps) { const [editingId, setEditingId] = createSignal(null); const [editingThresholds, setEditingThresholds] = createSignal>({}); + let searchInputRef: HTMLInputElement | undefined; + + // Set up keyboard shortcuts + onMount(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Skip if user is typing in an input or textarea (unless it's Escape) + const target = e.target as HTMLElement; + const isInInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.contentEditable === 'true'; + + // Escape clears search from anywhere + if (e.key === 'Escape') { + e.preventDefault(); + setSearchTerm(''); + if (searchInputRef && isInInput) { + searchInputRef.blur(); + } + return; + } + + // Skip other shortcuts if already in an input + if (isInInput) { + return; + } + + // Any letter/number focuses search and starts typing + if (e.key.length === 1 && e.key.match(/[a-z0-9]/i)) { + e.preventDefault(); + if (searchInputRef) { + searchInputRef.focus(); + setSearchTerm(e.key); + } + } + }; + + document.addEventListener('keydown', handleKeyDown); + + onCleanup(() => { + document.removeEventListener('keydown', handleKeyDown); + }); + }); + + // Helper function to format values with units + const formatMetricValue = (metric: string, value: number | undefined): string => { + if (value === undefined || value === null) return '0'; + + // Percentage-based metrics + if (metric === 'cpu' || metric === 'memory' || metric === 'disk') { + return `${value}%`; + } + + // MB/s metrics + if (metric === 'diskRead' || metric === 'diskWrite' || metric === 'networkIn' || metric === 'networkOut') { + return `${value} MB/s`; + } + + return String(value); + }; + + // Check if there's an active alert for a resource/metric + const hasActiveAlert = (resourceId: string, metric: string): boolean => { + if (!props.activeAlerts) return false; + const alertKey = `${resourceId}-${metric}`; + return alertKey in props.activeAlerts; + }; + + // Component for metric value with active alert indicator + const MetricValueWithHeat = (props: { + resourceId: string; + metric: string; + value: number; + isOverridden: boolean + }) => ( +
+ + {formatMetricValue(props.metric, props.value)} + + +
+ +
+ ); + // Combine all resources (guests and nodes) with their overrides const resourcesWithOverrides = createMemo(() => { const search = searchTerm().toLowerCase(); @@ -67,6 +158,14 @@ export function ThresholdsTable(props: ThresholdsTableProps) { const guestId = guest.id || `${guest.instance}-${guest.name}-${guest.vmid}`; const override = overridesMap.get(guestId); + // Check if any threshold values actually differ from defaults + const hasCustomThresholds = override?.thresholds && + Object.keys(override.thresholds).some(key => { + const k = key as keyof typeof override.thresholds; + return override.thresholds[k] !== undefined && + override.thresholds[k] !== (props.guestDefaults as any)[k]; + }); + return { id: guestId, name: guest.name, @@ -76,9 +175,10 @@ export function ThresholdsTable(props: ThresholdsTableProps) { node: guest.node, instance: guest.instance, status: guest.status, - hasOverride: !!override, + hasOverride: hasCustomThresholds || false, // Only true if thresholds differ disabled: override?.disabled || false, - thresholds: override?.thresholds || props.guestDefaults + thresholds: override?.thresholds || {}, + defaults: props.guestDefaults }; }); @@ -86,15 +186,25 @@ export function ThresholdsTable(props: ThresholdsTableProps) { const nodes = props.nodes.map(node => { const override = overridesMap.get(node.id); + // Check if any threshold values actually differ from defaults + const hasCustomThresholds = override?.thresholds && + Object.keys(override.thresholds).some(key => { + const k = key as keyof typeof override.thresholds; + return override.thresholds[k] !== undefined && + override.thresholds[k] !== (props.nodeDefaults as any)[k]; + }); + return { id: node.id, name: node.name, type: 'node' as const, resourceType: 'Node', status: node.status, - hasOverride: !!override, + hasOverride: hasCustomThresholds || false, // Only true if thresholds differ disabled: false, - thresholds: override?.thresholds || props.nodeDefaults + disableConnectivity: override?.disableConnectivity || false, + thresholds: override?.thresholds || {}, + defaults: props.nodeDefaults }; }); @@ -138,33 +248,43 @@ export function ThresholdsTable(props: ThresholdsTableProps) { return groups; }); - const startEditing = (resourceId: string, currentThresholds: any) => { + const startEditing = (resourceId: string, currentThresholds: any, defaults: any) => { setEditingId(resourceId); - setEditingThresholds(currentThresholds); + // Merge defaults with overrides for editing + const mergedThresholds = { ...defaults, ...currentThresholds }; + setEditingThresholds(mergedThresholds); }; const saveEdit = (resourceId: string) => { const resource = resourcesWithOverrides().find(r => r.id === resourceId); if (!resource) return; - const thresholds = editingThresholds(); + const editedThresholds = editingThresholds(); + const defaultThresholds = resource.defaults; - // Check if there are any actual changes from the defaults - const defaultThresholds = resource.type === 'guest' ? props.guestDefaults : props.nodeDefaults; - const hasChanges = Object.keys(thresholds).some(key => { - const editedValue = thresholds[key]; + // Only include values that differ from defaults + const overrideThresholds: Record = {}; + Object.keys(editedThresholds).forEach(key => { + const editedValue = editedThresholds[key]; const defaultValue = defaultThresholds[key as keyof typeof defaultThresholds]; - return editedValue !== defaultValue; + if (editedValue !== defaultValue && editedValue !== undefined && editedValue !== '') { + overrideThresholds[key] = editedValue; + } }); - // If no changes and no existing override, just cancel the edit - if (!hasChanges && !resource.hasOverride) { - cancelEdit(); - return; - } - - // If no changes but there's an existing override, keep it as is - if (!hasChanges && resource.hasOverride) { + // If no overrides, just cancel the edit + if (Object.keys(overrideThresholds).length === 0) { + // If there was an existing override, remove it + if (resource.hasOverride) { + const newOverrides = props.overrides().filter(o => o.id !== resourceId); + props.setOverrides(newOverrides); + + // Also remove from raw config + const newRawConfig = { ...props.rawOverridesConfig() }; + delete newRawConfig[resourceId]; + props.setRawOverridesConfig(newRawConfig); + props.setHasUnsavedChanges(true); + } cancelEdit(); return; } @@ -179,7 +299,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) { node: 'node' in resource ? resource.node : undefined, instance: 'instance' in resource ? resource.instance : undefined, disabled: resource.disabled, - thresholds + thresholds: overrideThresholds }; // Update overrides list @@ -195,7 +315,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) { // Update raw config const newRawConfig = { ...props.rawOverridesConfig() }; const hysteresisThresholds: Record = {}; - Object.entries(thresholds).forEach(([metric, value]) => { + Object.entries(overrideThresholds).forEach(([metric, value]) => { if (value !== undefined && value !== null) { hysteresisThresholds[metric] = { trigger: value, @@ -233,41 +353,155 @@ export function ThresholdsTable(props: ThresholdsTableProps) { 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 - }; + // Get existing override if it exists + const existingOverride = props.overrides().find(o => o.id === resourceId); - const existingIndex = props.overrides().findIndex(o => o.id === resourceId); - if (existingIndex >= 0) { - const newOverrides = [...props.overrides()]; - newOverrides[existingIndex] = override; - props.setOverrides(newOverrides); + console.log('Toggle disabled for:', resourceId); + console.log('Existing override:', existingOverride); + console.log('Existing thresholds:', existingOverride?.thresholds); + console.log('Threshold keys:', existingOverride ? Object.keys(existingOverride.thresholds || {}) : 'no override'); + + // Determine the current disabled state from the override (or false if no override) + const currentDisabledState = existingOverride?.disabled || false; + const newDisabledState = !currentDisabledState; + + console.log('Current disabled state:', currentDisabledState); + console.log('New disabled state:', newDisabledState); + + // Clean the thresholds to exclude 'disabled' if it got in there + const cleanThresholds: any = { ...(existingOverride?.thresholds || {}) }; + delete cleanThresholds.disabled; + + // If enabling (disabled = false) and no custom thresholds exist, remove the override entirely + if (!newDisabledState && (!existingOverride || Object.keys(cleanThresholds).length === 0)) { + console.log('REMOVING OVERRIDE - enabling with no custom thresholds'); + // Remove the override completely + props.setOverrides(props.overrides().filter(o => o.id !== resourceId)); + + // Remove from raw config + const newRawConfig = { ...props.rawOverridesConfig() }; + delete newRawConfig[resourceId]; + props.setRawOverridesConfig(newRawConfig); } else { - props.setOverrides([...props.overrides(), override]); + console.log('UPDATING OVERRIDE - either disabling or has custom thresholds'); + + 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: newDisabledState, + thresholds: cleanThresholds // Only keep actual threshold overrides + }; + + 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 = {}; + + // Only add threshold overrides that differ from defaults + Object.entries(override.thresholds).forEach(([metric, value]) => { + if (value !== undefined && value !== null) { + hysteresisThresholds[metric] = { + trigger: value, + clear: Math.max(0, (value as number) - 5) + }; + } + }); + + if (newDisabledState) { + hysteresisThresholds.disabled = true; + } + + newRawConfig[resourceId] = hysteresisThresholds; + props.setRawOverridesConfig(newRawConfig); } - // 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) - }; + props.setHasUnsavedChanges(true); + }; + + const toggleNodeConnectivity = (nodeId: string) => { + console.log('toggleNodeConnectivity called for:', nodeId); + const node = resourcesWithOverrides().find(r => r.id === nodeId); + console.log('Found node:', node); + if (!node || node.type !== 'node') return; + + // Get existing override if it exists + const existingOverride = props.overrides().find(o => o.id === nodeId); + console.log('Existing override:', existingOverride); + + // Determine the current state + const currentDisableConnectivity = existingOverride?.disableConnectivity || false; + const newDisableConnectivity = !currentDisableConnectivity; + console.log('Current state:', currentDisableConnectivity, 'New state:', newDisableConnectivity); + + // Clean the thresholds to exclude any unwanted fields + const cleanThresholds: any = { ...(existingOverride?.thresholds || {}) }; + delete cleanThresholds.disabled; + delete cleanThresholds.disableConnectivity; + + // If enabling connectivity alerts (disableConnectivity = false) and no custom thresholds exist, remove the override entirely + if (!newDisableConnectivity && Object.keys(cleanThresholds).length === 0) { + // Remove the override completely + props.setOverrides(props.overrides().filter(o => o.id !== nodeId)); + + // Remove from raw config + const newRawConfig = { ...props.rawOverridesConfig() }; + delete newRawConfig[nodeId]; + props.setRawOverridesConfig(newRawConfig); + } else { + // Update or create the override + const override: Override = { + id: nodeId, + name: node.name, + type: node.type, + resourceType: node.resourceType, + disableConnectivity: newDisableConnectivity, + thresholds: cleanThresholds + }; + + // Update overrides list + const existingIndex = props.overrides().findIndex(o => o.id === nodeId); + if (existingIndex >= 0) { + const newOverrides = [...props.overrides()]; + newOverrides[existingIndex] = override; + props.setOverrides(newOverrides); + } else { + props.setOverrides([...props.overrides(), override]); } - }); - hysteresisThresholds.disabled = !resource.disabled; - newRawConfig[resourceId] = hysteresisThresholds; - props.setRawOverridesConfig(newRawConfig); + + // Update raw config + const newRawConfig = { ...props.rawOverridesConfig() }; + const hysteresisThresholds: Record = {}; + + // Add threshold configs + Object.entries(cleanThresholds).forEach(([metric, value]) => { + if (value !== undefined && value !== null) { + hysteresisThresholds[metric] = { + trigger: value, + clear: Math.max(0, (value as number) - 5) + }; + } + }); + + if (newDisableConnectivity) { + hysteresisThresholds.disableConnectivity = true; + } + + newRawConfig[nodeId] = hysteresisThresholds; + props.setRawOverridesConfig(newRawConfig); + } props.setHasUnsavedChanges(true); }; @@ -297,77 +531,31 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
-
- {/* Compact grid layout */} -
- {/* Left column - Time threshold and reset button */} -
-
- -
- { - props.setTimeThreshold(parseInt(e.currentTarget.value) || 0); - props.setHasUnsavedChanges(true); - }} - class="w-16 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" - /> - - sec {props.timeThreshold() === 0 ? '(disabled)' : `(wait ${props.timeThreshold()}s before alerting)`} - -
-
- - -
- - {/* Right column - Threshold values in compact table */} +
+ {/* Threshold table */} +
+

+ Default thresholds for all resources. Individual resources can override these values below. +

- - - - - + + + + + + + + + - - - + + - - + + + + + - - - - + + - - - + + + + + - - - - - + + + + + + + +
TypeCPU %Memory %Disk %Storage %Resource TypeCPU
%
Memory
%
Disk
%
Storage
%
Disk Read
MB/s
Disk Write
MB/s
Net In
MB/s
Net Out
MB/s
VMs & Containers +
VMs & Containers ({...prev, cpu: parseInt(e.currentTarget.value) || 0})); props.setHasUnsavedChanges(true); }} - 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" + class="w-16 px-2 py-1 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 focus:ring-2 focus:ring-blue-500 focus:border-transparent" /> + ({...prev, memory: parseInt(e.currentTarget.value) || 0})); props.setHasUnsavedChanges(true); }} - 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" + class="w-16 px-2 py-1 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 focus:ring-2 focus:ring-blue-500 focus:border-transparent" /> + ({...prev, disk: parseInt(e.currentTarget.value) || 0})); props.setHasUnsavedChanges(true); }} - 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" + class="w-16 px-2 py-1 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 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> + - + { + props.setGuestDefaults((prev) => ({...prev, diskRead: parseInt(e.currentTarget.value) || 0})); + props.setHasUnsavedChanges(true); + }} + class="w-16 px-2 py-1 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 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> + + { + props.setGuestDefaults((prev) => ({...prev, diskWrite: parseInt(e.currentTarget.value) || 0})); + props.setHasUnsavedChanges(true); + }} + class="w-16 px-2 py-1 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 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> + + { + props.setGuestDefaults((prev) => ({...prev, networkIn: parseInt(e.currentTarget.value) || 0})); + props.setHasUnsavedChanges(true); + }} + class="w-16 px-2 py-1 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 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> + + { + props.setGuestDefaults((prev) => ({...prev, networkOut: parseInt(e.currentTarget.value) || 0})); + props.setHasUnsavedChanges(true); + }} + class="w-16 px-2 py-1 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 focus:ring-2 focus:ring-blue-500 focus:border-transparent" /> -
Proxmox Nodes +
Proxmox Nodes ({...prev, cpu: parseInt(e.currentTarget.value) || 0})); props.setHasUnsavedChanges(true); }} - 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" + class="w-16 px-2 py-1 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 focus:ring-2 focus:ring-blue-500 focus:border-transparent" /> + ({...prev, memory: parseInt(e.currentTarget.value) || 0})); props.setHasUnsavedChanges(true); }} - 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" + class="w-16 px-2 py-1 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 focus:ring-2 focus:ring-blue-500 focus:border-transparent" /> + ({...prev, disk: parseInt(e.currentTarget.value) || 0})); props.setHasUnsavedChanges(true); }} - 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" + class="w-16 px-2 py-1 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 focus:ring-2 focus:ring-blue-500 focus:border-transparent" /> ------
Storage--- + Storage--- ----
+ + {/* Additional settings row */} +
+
+ + { + props.setTimeThreshold(parseInt(e.currentTarget.value) || 0); + props.setHasUnsavedChanges(true); + }} + class="w-14 px-1 py-0.5 text-xs border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" + /> + + seconds before alerting + +
+ + +
@@ -488,6 +793,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) { {/* Search Bar */}
+ + +
{/* Resources Table */} @@ -525,6 +841,18 @@ export function ThresholdsTable(props: ThresholdsTableProps) { Disk % + + Disk R
MB/s + + + Disk W
MB/s + + + Net In
Mbps + + + Net Out
Mbps + Alerts @@ -544,7 +872,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) { <> {/* Group header */} - + {groupName} @@ -553,6 +881,23 @@ export function ThresholdsTable(props: ThresholdsTableProps) { {(resource) => { const isEditing = () => editingId() === resource.id; const thresholds = () => isEditing() ? editingThresholds() : resource.thresholds; + const displayValue = (metric: string) => { + if (isEditing()) return thresholds()[metric] || resource.defaults[metric] || ''; + // Show override value or default + return resource.thresholds[metric] || resource.defaults[metric] || 0; + }; + const shouldShowMetric = (metric: string) => { + // Nodes don't have I/O metrics + if (resource.type === 'node' && + (metric === 'diskRead' || metric === 'diskWrite' || + metric === 'networkIn' || metric === 'networkOut')) { + return false; + } + return true; + }; + const isOverridden = (metric: string) => { + return resource.thresholds[metric] !== undefined && resource.thresholds[metric] !== null; + }; return ( @@ -564,7 +909,13 @@ export function ThresholdsTable(props: ThresholdsTableProps) { ({resource.vmid}) - + { + const override = props.overrides().find(o => o.id === resource.id); + if (!override) return false; + // Show badge if there are threshold overrides or connectivity is disabled for nodes + return Object.keys(override.thresholds).length > 0 || + (resource.type === 'node' && resource.disableConnectivity); + })()}> Custom @@ -593,9 +944,12 @@ export function ThresholdsTable(props: ThresholdsTableProps) { - {thresholds().cpu || '-'} - + }> - {thresholds().memory || '-'} - + }> - {thresholds().disk || '-'} - + }> + + - + }> + + + }> + - + }> + setEditingThresholds({ + ...editingThresholds(), + diskRead: 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(), + diskWrite: 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(), + networkIn: 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(), + networkOut: 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" + /> + + + - - + @@ -673,7 +1173,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) { - + o.id === resource.id)}>