From 6cf3c053a4103f43eb2c403cfd15cc361191616d Mon Sep 17 00:00:00 2001 From: Pulse Monitor Date: Wed, 10 Sep 2025 20:20:38 +0000 Subject: [PATCH] fix: resolve PBS threshold save issues and input focus loss (addresses #440) - Added 'pbs' type to Override interfaces in both Alerts.tsx and ThresholdsTable.tsx - Fixed createEffect in Alerts.tsx to properly handle PBS server overrides - Prevented memos from recomputing during editing to avoid input focus loss - PBS threshold values now persist correctly when saved --- .../src/components/Alerts/ResourceTable.tsx | 3 + .../src/components/Alerts/ThresholdsTable.tsx | 46 +++++++-- frontend-modern/src/pages/Alerts.tsx | 94 +++++++++++-------- 3 files changed, 99 insertions(+), 44 deletions(-) diff --git a/frontend-modern/src/components/Alerts/ResourceTable.tsx b/frontend-modern/src/components/Alerts/ResourceTable.tsx index 60b44503b..0ae400c9d 100644 --- a/frontend-modern/src/components/Alerts/ResourceTable.tsx +++ b/frontend-modern/src/components/Alerts/ResourceTable.tsx @@ -123,6 +123,9 @@ export function ResourceTable(props: ResourceTableProps) { const val = thresh?.[metric] || defaults[metric]; return typeof val === 'string' ? (parseFloat(val) || 0) : (val || 0); } + if (resource.type === 'pbs') { + console.log(`PBS Display - Resource: ${resource.id}, Metric: ${metric}, Thresholds:`, resource.thresholds, 'Defaults:', defaults); + } return resource.thresholds?.[metric] || defaults[metric] || 0; }; const isOverridden = (metric: string) => { diff --git a/frontend-modern/src/components/Alerts/ThresholdsTable.tsx b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx index 48cfab7e5..32c353a5e 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTable.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx @@ -5,7 +5,7 @@ import { ResourceTable } from './ResourceTable'; interface Override { id: string; name: string; - type: 'guest' | 'node' | 'storage'; + type: 'guest' | 'node' | 'storage' | 'pbs'; resourceType?: string; vmid?: number; node?: string; @@ -130,7 +130,12 @@ export function ThresholdsTable(props: ThresholdsTableProps) { }; // Process nodes with their overrides - const nodesWithOverrides = createMemo(() => { + const nodesWithOverrides = createMemo((prev) => { + // If we're currently editing, return the previous value to avoid re-renders + if (editingId()) { + return prev || []; + } + const search = searchTerm().toLowerCase(); const overridesMap = new Map(props.overrides().map(o => [o.id, o])); @@ -166,7 +171,12 @@ export function ThresholdsTable(props: ThresholdsTableProps) { }); // Process guests with their overrides and group by node - const guestsGroupedByNode = createMemo(() => { + const guestsGroupedByNode = createMemo((prev) => { + // If we're currently editing, return the previous value to avoid re-renders + if (editingId()) { + return prev || {}; + } + const search = searchTerm().toLowerCase(); const overridesMap = new Map(props.overrides().map(o => [o.id, o])); @@ -231,9 +241,15 @@ export function ThresholdsTable(props: ThresholdsTableProps) { }); // Process PBS servers with their overrides - const pbsServersWithOverrides = createMemo(() => { + const pbsServersWithOverrides = createMemo((prev) => { + // If we're currently editing, return the previous value to avoid re-renders + if (editingId()) { + return prev || []; + } + const search = searchTerm().toLowerCase(); const overridesMap = new Map(props.overrides().map(o => [o.id, o])); + console.log('PBS memo recomputing, overrides:', Array.from(overridesMap.entries())); // Get PBS instances from props const pbsInstances = props.pbsInstances || []; @@ -285,7 +301,12 @@ export function ThresholdsTable(props: ThresholdsTableProps) { }); // Process storage with their overrides - const storageWithOverrides = createMemo(() => { + const storageWithOverrides = createMemo((prev) => { + // If we're currently editing, return the previous value to avoid re-renders + if (editingId()) { + return prev || []; + } + const search = searchTerm().toLowerCase(); const overridesMap = new Map(props.overrides().map(o => [o.id, o])); @@ -336,23 +357,33 @@ export function ThresholdsTable(props: ThresholdsTableProps) { const allGuests = Object.values(guestsGroupedByNode()).flat(); const allResources = [...nodesWithOverrides(), ...allGuests, ...storageWithOverrides(), ...pbsServersWithOverrides()]; const resource = allResources.find(r => r.id === resourceId); - if (!resource) return; + if (!resource) { + console.log('Resource not found for id:', resourceId); + return; + } + console.log('SAVING PBS THRESHOLD FOR:', resourceId, 'Type:', resource.type); const editedThresholds = editingThresholds(); const defaultThresholds = resource.defaults; + console.log('Edited thresholds:', editedThresholds); + console.log('Default thresholds:', defaultThresholds); // 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]; + console.log(`Comparing ${key}: edited=${editedValue}, default=${defaultValue}`); if (editedValue !== defaultValue && editedValue !== undefined && editedValue !== '') { overrideThresholds[key] = editedValue; } }); + console.log('Override thresholds to save:', overrideThresholds); + // If no overrides, just cancel the edit if (Object.keys(overrideThresholds).length === 0) { + console.log('No overrides to save, canceling edit'); // If there was an existing override, remove it if (resource.hasOverride) { const newOverrides = props.overrides().filter(o => o.id !== resourceId); @@ -372,7 +403,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) { const override: Override = { id: resourceId, name: resource.name, - type: resource.type as 'guest' | 'node' | 'storage', + type: resource.type as 'guest' | 'node' | 'storage' | 'pbs', resourceType: resource.resourceType, vmid: 'vmid' in resource ? resource.vmid : undefined, node: 'node' in resource ? resource.node : undefined, @@ -409,6 +440,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) { props.setRawOverridesConfig(newRawConfig); props.setHasUnsavedChanges(true); + console.log('Save complete, clearing edit state'); setEditingId(null); setEditingThresholds({}); }; diff --git a/frontend-modern/src/pages/Alerts.tsx b/frontend-modern/src/pages/Alerts.tsx index a6d003e11..4896babc6 100644 --- a/frontend-modern/src/pages/Alerts.tsx +++ b/frontend-modern/src/pages/Alerts.tsx @@ -45,10 +45,10 @@ interface ScheduleConfig { // Override interface for both guests and nodes interface Override { - id: string; // Full ID (e.g. "Main-node1-105" for guest, "node-node1" for node) + id: string; // Full ID (e.g. "Main-node1-105" for guest, "node-node1" for node, "pbs-name" for PBS) name: string; // Display name - type: 'guest' | 'node' | 'storage'; - resourceType?: string; // VM, CT, Node, or Storage + type: 'guest' | 'node' | 'storage' | 'pbs'; + resourceType?: string; // VM, CT, Node, Storage, or PBS vmid?: number; // Only for guests node?: string; // Node name (for guests and storage), undefined for nodes themselves instance?: string; @@ -179,54 +179,74 @@ export function Alerts() { // Process raw overrides config when state changes createEffect(() => { + // Skip this effect if there are unsaved changes to prevent losing focus + if (hasUnsavedChanges()) { + return; + } + const rawConfig = rawOverridesConfig(); if (Object.keys(rawConfig).length > 0 && state.nodes && state.vms && state.containers && state.storage) { // Convert overrides object to array format const overridesList: Override[] = []; Object.entries(rawConfig).forEach(([key, thresholds]) => { - // Check if it's a node override by looking for matching node - const node = (state.nodes || []).find((n) => n.id === key); - if (node) { - overridesList.push({ - id: key, - name: node.name, - type: 'node', - resourceType: 'Node', - disableConnectivity: thresholds.disableConnectivity || false, - thresholds: extractTriggerValues(thresholds) - }); - } else { - // Check if it's a storage device - const storage = (state.storage || []).find((s) => s.id === key); - if (storage) { + // Check if it's a PBS server override (starts with "pbs-") + if (key.startsWith('pbs-')) { + const pbs = (state.pbs || []).find((p) => p.id === key); + if (pbs) { overridesList.push({ id: key, - name: storage.name, - type: 'storage', - resourceType: 'Storage', - node: storage.node, - instance: storage.instance, - disabled: thresholds.disabled || false, + name: pbs.name, + type: 'pbs', + resourceType: 'PBS', + disableConnectivity: thresholds.disableConnectivity || false, + thresholds: extractTriggerValues(thresholds) + }); + } + } else { + // Check if it's a node override by looking for matching node + const node = (state.nodes || []).find((n) => n.id === key); + if (node) { + overridesList.push({ + id: key, + name: node.name, + type: 'node', + resourceType: 'Node', + disableConnectivity: thresholds.disableConnectivity || false, thresholds: extractTriggerValues(thresholds) }); } else { - // Find the guest by matching the full ID - const vm = (state.vms || []).find((g) => g.id === key); - const container = (state.containers || []).find((g) => g.id === key); - const guest = vm || container; - if (guest) { + // Check if it's a storage device + const storage = (state.storage || []).find((s) => s.id === key); + if (storage) { overridesList.push({ id: key, - name: guest.name, - type: 'guest', - resourceType: guest.type === 'qemu' ? 'VM' : 'CT', - vmid: guest.vmid, - node: guest.node, - instance: guest.instance, + name: storage.name, + type: 'storage', + resourceType: 'Storage', + node: storage.node, + instance: storage.instance, disabled: thresholds.disabled || false, thresholds: extractTriggerValues(thresholds) }); + } else { + // Find the guest by matching the full ID + const vm = (state.vms || []).find((g) => g.id === key); + const container = (state.containers || []).find((g) => g.id === key); + const guest = vm || container; + if (guest) { + overridesList.push({ + id: key, + name: guest.name, + type: 'guest', + resourceType: guest.type === 'qemu' ? 'VM' : 'CT', + vmid: guest.vmid, + node: guest.node, + instance: guest.instance, + disabled: thresholds.disabled || false, + thresholds: extractTriggerValues(thresholds) + }); + } } } } @@ -238,9 +258,9 @@ export function Alerts() { overridesList.some((newOverride) => { const existing = currentOverrides.find(o => o.id === newOverride.id); if (!existing) return true; - // Check both thresholds and disableConnectivity for nodes + // Check both thresholds and disableConnectivity for nodes/PBS const thresholdsChanged = JSON.stringify(newOverride.thresholds) !== JSON.stringify(existing.thresholds); - const connectivityChanged = newOverride.type === 'node' && newOverride.disableConnectivity !== existing.disableConnectivity; + const connectivityChanged = (newOverride.type === 'node' || newOverride.type === 'pbs') && newOverride.disableConnectivity !== existing.disableConnectivity; const disabledChanged = (newOverride.type === 'guest' || newOverride.type === 'storage') && newOverride.disabled !== existing.disabled; return thresholdsChanged || connectivityChanged || disabledChanged; });