fix: implement batch toggle operations to prevent state sync issues

addresses #349 - fixed the issue where toggling all Proxmox Node alerts would skip some nodes on subsequent clicks. The problem was that multiple toggle operations in a loop were reading from the same state snapshot.

- implemented batchToggleNodeConnectivity and batchToggleDisabled functions
- these functions collect all changes and apply them atomically
- ensures all resources are properly toggled to the target state
- fixes the issue where individual nodes (like 'pi') weren't toggling correctly
This commit is contained in:
Pulse Monitor
2025-08-25 14:41:26 +00:00
parent 000f125f01
commit 67486d75f7
2 changed files with 153 additions and 19 deletions
@@ -13,7 +13,8 @@ interface ResourceTableProps {
onRemoveOverride: (resourceId: string) => void;
onToggleDisabled?: (resourceId: string) => void;
onToggleNodeConnectivity?: (nodeId: string) => void;
onBatchToggleAlerts?: (resourceIds: string[], enable: boolean) => void;
onBatchToggleNodeConnectivity?: (nodeIds: string[], targetState: boolean) => void;
onBatchToggleDisabled?: (resourceIds: string[], targetState: boolean) => void;
editingId: () => string | null;
editingThresholds: () => Record<string, any>;
setEditingThresholds: (value: Record<string, any>) => void;
@@ -100,27 +101,20 @@ export function ResourceTable(props: ResourceTableProps) {
onClick={() => {
const allDisabled = areAllAlertsDisabled();
const resourceIds = getAllResourceIds();
if (props.title === 'Proxmox Nodes' && props.onToggleNodeConnectivity) {
if (props.title === 'Proxmox Nodes' && props.onBatchToggleNodeConnectivity) {
// For nodes, toggle connectivity alerts
// If all are disabled, enable all. If any are enabled, disable all.
const targetState = !allDisabled; // true = disable alerts, false = enable alerts
resourceIds.forEach(id => {
const resource = props.resources?.find(r => r.id === id);
if (resource && resource.disableConnectivity !== targetState) {
props.onToggleNodeConnectivity!(id);
}
});
} else if (props.onToggleDisabled) {
// Use batch toggle to update all at once
props.onBatchToggleNodeConnectivity(resourceIds, targetState);
} else if (props.onBatchToggleDisabled) {
// For guests and storage, toggle disabled flag
// If all are disabled, enable all. If any are enabled, disable all.
const targetState = !allDisabled; // true = disable alerts, false = enable alerts
resourceIds.forEach(id => {
const resources = props.groupedResources ? Object.values(props.groupedResources).flat() : (props.resources || []);
const resource = resources.find(r => r.id === id);
if (resource && resource.disabled !== targetState) {
props.onToggleDisabled!(id);
}
});
// Use batch toggle to update all at once
props.onBatchToggleDisabled(resourceIds, targetState);
}
}}
class={`p-0.5 rounded transition-colors ${
@@ -425,6 +425,76 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
props.setHasUnsavedChanges(true);
};
const batchToggleDisabled = (resourceIds: string[], targetState: boolean) => {
const currentOverrides = [...props.overrides()];
const newRawConfig = { ...props.rawOverridesConfig() };
const allGuests = Object.values(guestsGroupedByNode()).flat();
const allResources = [...allGuests, ...storageWithOverrides()];
resourceIds.forEach(resourceId => {
const resource = allResources.find(r => r.id === resourceId);
if (!resource || (resource.type !== 'guest' && resource.type !== 'storage')) return;
const existingOverrideIndex = currentOverrides.findIndex(o => o.id === resourceId);
const existingOverride = existingOverrideIndex >= 0 ? currentOverrides[existingOverrideIndex] : undefined;
// Clean the thresholds
const cleanThresholds: any = { ...(existingOverride?.thresholds || {}) };
delete cleanThresholds.disabled;
// If enabling (targetState = false) and no custom thresholds exist, remove the override
if (!targetState && Object.keys(cleanThresholds).length === 0) {
// Remove from overrides array
if (existingOverrideIndex >= 0) {
currentOverrides.splice(existingOverrideIndex, 1);
}
// Remove from raw config
delete newRawConfig[resourceId];
} else {
// Create or update the 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: targetState,
thresholds: cleanThresholds
};
if (existingOverrideIndex >= 0) {
currentOverrides[existingOverrideIndex] = override;
} else {
currentOverrides.push(override);
}
// Update raw config
const hysteresisThresholds: Record<string, any> = {};
Object.entries(cleanThresholds).forEach(([metric, value]) => {
if (value !== undefined && value !== null) {
hysteresisThresholds[metric] = {
trigger: value,
clear: Math.max(0, (value as number) - 5)
};
}
});
if (targetState) {
hysteresisThresholds.disabled = true;
}
newRawConfig[resourceId] = hysteresisThresholds;
}
});
// Apply all changes at once
props.setOverrides(currentOverrides);
props.setRawOverridesConfig(newRawConfig);
props.setHasUnsavedChanges(true);
};
const toggleDisabled = (resourceId: string, forceState?: boolean) => {
// Flatten grouped guests to find the resource
const allGuests = Object.values(guestsGroupedByNode()).flat();
@@ -500,15 +570,82 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
props.setHasUnsavedChanges(true);
};
const batchToggleNodeConnectivity = (nodeIds: string[], targetState: boolean) => {
const currentOverrides = [...props.overrides()];
const newRawConfig = { ...props.rawOverridesConfig() };
const nodes = nodesWithOverrides();
nodeIds.forEach(nodeId => {
const node = nodes.find(r => r.id === nodeId);
if (!node || node.type !== 'node') return;
const existingOverrideIndex = currentOverrides.findIndex(o => o.id === nodeId);
const existingOverride = existingOverrideIndex >= 0 ? currentOverrides[existingOverrideIndex] : undefined;
// Clean the thresholds
const cleanThresholds: any = { ...(existingOverride?.thresholds || {}) };
delete cleanThresholds.disabled;
delete cleanThresholds.disableConnectivity;
// If enabling connectivity alerts (targetState = false) and no custom thresholds exist, remove the override
if (!targetState && Object.keys(cleanThresholds).length === 0) {
// Remove from overrides array
if (existingOverrideIndex >= 0) {
currentOverrides.splice(existingOverrideIndex, 1);
}
// Remove from raw config
delete newRawConfig[nodeId];
} else {
// Create or update the override
const override: Override = {
id: nodeId,
name: node.name,
type: node.type,
resourceType: node.resourceType,
disableConnectivity: targetState,
thresholds: cleanThresholds
};
if (existingOverrideIndex >= 0) {
currentOverrides[existingOverrideIndex] = override;
} else {
currentOverrides.push(override);
}
// Update raw config
const hysteresisThresholds: Record<string, any> = {};
Object.entries(cleanThresholds).forEach(([metric, value]) => {
if (value !== undefined && value !== null) {
hysteresisThresholds[metric] = {
trigger: value,
clear: Math.max(0, (value as number) - 5)
};
}
});
if (targetState) {
hysteresisThresholds.disableConnectivity = true;
}
newRawConfig[nodeId] = hysteresisThresholds;
}
});
// Apply all changes at once
props.setOverrides(currentOverrides);
props.setRawOverridesConfig(newRawConfig);
props.setHasUnsavedChanges(true);
};
const toggleNodeConnectivity = (nodeId: string, forceState?: boolean) => {
const node = nodesWithOverrides().find(r => r.id === nodeId);
if (!node || node.type !== 'node') return;
// Get existing override if it exists
// Get existing override if it exists
const existingOverride = props.overrides().find(o => o.id === nodeId);
// Determine the current state
const currentDisableConnectivity = existingOverride?.disableConnectivity || false;
// Determine the current state - use the node's computed state, not just the override
const currentDisableConnectivity = node.disableConnectivity;
const newDisableConnectivity = forceState !== undefined ? forceState : !currentDisableConnectivity;
// Clean the thresholds to exclude any unwanted fields
@@ -894,6 +1031,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
onCancelEdit={cancelEdit}
onRemoveOverride={removeOverride}
onToggleNodeConnectivity={toggleNodeConnectivity}
onBatchToggleNodeConnectivity={batchToggleNodeConnectivity}
editingId={editingId}
editingThresholds={editingThresholds}
setEditingThresholds={setEditingThresholds}
@@ -933,6 +1071,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
onCancelEdit={cancelEdit}
onRemoveOverride={removeOverride}
onToggleDisabled={toggleDisabled}
onBatchToggleDisabled={batchToggleDisabled}
editingId={editingId}
editingThresholds={editingThresholds}
setEditingThresholds={setEditingThresholds}
@@ -953,6 +1092,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
onCancelEdit={cancelEdit}
onRemoveOverride={removeOverride}
onToggleDisabled={toggleDisabled}
onBatchToggleDisabled={batchToggleDisabled}
editingId={editingId}
editingThresholds={editingThresholds}
setEditingThresholds={setEditingThresholds}