From d9a26fb797dc0d705ee265400741de15cce769e5 Mon Sep 17 00:00:00 2001 From: Pulse Monitor Date: Wed, 27 Aug 2025 14:54:44 +0000 Subject: [PATCH] fix: address #357 - persist schedule settings correctly in alerts UI The schedule settings (quiet hours, grouping, escalation) were not persisting because the component state was lost when switching tabs. Lifted the state to the parent component so it persists across tab switches and saves correctly. --- frontend-modern/src/pages/Alerts.tsx | 289 ++++++++++++++------------- frontend-modern/src/types/alerts.ts | 32 ++- 2 files changed, 176 insertions(+), 145 deletions(-) diff --git a/frontend-modern/src/pages/Alerts.tsx b/frontend-modern/src/pages/Alerts.tsx index f82990d3f..9efb0adfb 100644 --- a/frontend-modern/src/pages/Alerts.tsx +++ b/frontend-modern/src/pages/Alerts.tsx @@ -18,8 +18,9 @@ interface DestinationsRef { emailConfig?: () => EmailConfig; } +// ScheduleConfig interface - used for loading schedule configuration +// @ts-ignore - used in type annotations interface ScheduleConfig { - enabled?: boolean; quietHours?: { enabled: boolean; start: string; @@ -38,13 +39,10 @@ interface ScheduleConfig { maxAlertsHour?: number; escalation?: { enabled: boolean; + levels?: Array<{ after: number; notify: string }>; }; } -interface ScheduleRef { - setScheduleConfig?: (config: ScheduleConfig) => void; - getScheduleConfig?: () => ScheduleConfig | undefined; -} // Override interface for both guests and nodes interface Override { @@ -105,7 +103,6 @@ export function Alerts() { // Store references to child component data let destinationsRef: DestinationsRef = {}; - let scheduleRef: ScheduleRef = {}; const [overrides, setOverrides] = createSignal([]); const [rawOverridesConfig, setRawOverridesConfig] = createSignal>({}); // Store raw config @@ -127,6 +124,41 @@ export function Alerts() { retryDelay: 5, rateLimit: 60 }); + + // Schedule configuration state moved to parent to persist across tab changes + const [scheduleQuietHours, setScheduleQuietHours] = createSignal({ + enabled: false, + start: '22:00', + end: '08:00', + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC', + days: { + monday: true, + tuesday: true, + wednesday: true, + thursday: true, + friday: true, + saturday: false, + sunday: false + } as Record + }); + + const [scheduleCooldown, setScheduleCooldown] = createSignal({ + enabled: true, + minutes: 30, + maxAlerts: 3 + }); + + const [scheduleGrouping, setScheduleGrouping] = createSignal({ + enabled: true, + window: 5, + byNode: true, + byGuest: false + }); + + const [scheduleEscalation, setScheduleEscalation] = createSignal({ + enabled: false, + levels: [] as Array<{ after: number; notify: string }> + }); // Set up destinationsRef.emailConfig function immediately destinationsRef.emailConfig = () => { @@ -254,27 +286,74 @@ export function Alerts() { // Store raw config to be processed when state is available setRawOverridesConfig(config.overrides); } - // Pass schedule config to schedule tab if it exists - if (config.schedule && scheduleRef.setScheduleConfig) { - // Convert days array to Record if needed - const scheduleConfig: ScheduleConfig = { - ...config.schedule, - quietHours: config.schedule.quietHours ? { - ...config.schedule.quietHours, - days: Array.isArray(config.schedule.quietHours.days) - ? { - '0': config.schedule.quietHours.days.includes(0), - '1': config.schedule.quietHours.days.includes(1), - '2': config.schedule.quietHours.days.includes(2), - '3': config.schedule.quietHours.days.includes(3), - '4': config.schedule.quietHours.days.includes(4), - '5': config.schedule.quietHours.days.includes(5), - '6': config.schedule.quietHours.days.includes(6), - } - : config.schedule.quietHours.days - } : undefined - }; - scheduleRef.setScheduleConfig(scheduleConfig); + // Load schedule config into parent state + if (config.schedule) { + if (config.schedule.quietHours) { + const qh = config.schedule.quietHours; + // Convert days array to object if needed + let days: Record; + if (Array.isArray(qh.days)) { + days = { + sunday: qh.days.includes(0), + monday: qh.days.includes(1), + tuesday: qh.days.includes(2), + wednesday: qh.days.includes(3), + thursday: qh.days.includes(4), + friday: qh.days.includes(5), + saturday: qh.days.includes(6) + }; + } else { + days = qh.days as Record || { + monday: true, + tuesday: true, + wednesday: true, + thursday: true, + friday: true, + saturday: false, + sunday: false + }; + } + + setScheduleQuietHours({ + enabled: qh.enabled || false, + start: qh.start || '22:00', + end: qh.end || '08:00', + timezone: qh.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC', + days + }); + } + + if (config.schedule.cooldown !== undefined) { + setScheduleCooldown({ + enabled: config.schedule.cooldown > 0, + minutes: config.schedule.cooldown, + maxAlerts: config.schedule.maxAlertsHour || 3 + }); + } + + if (config.schedule.grouping) { + setScheduleGrouping({ + enabled: config.schedule.grouping.enabled || false, + window: Math.floor((config.schedule.grouping.window || 300) / 60), // Convert seconds to minutes + byNode: config.schedule.grouping.byNode !== undefined ? config.schedule.grouping.byNode : true, + byGuest: config.schedule.grouping.byGuest !== undefined ? config.schedule.grouping.byGuest : false + }); + } else if (config.schedule.groupingWindow !== undefined) { + // Handle legacy groupingWindow field + setScheduleGrouping({ + enabled: config.schedule.groupingWindow > 0, + window: Math.floor(config.schedule.groupingWindow / 60), + byNode: true, + byGuest: false + }); + } + + if (config.schedule.escalation) { + setScheduleEscalation({ + enabled: config.schedule.escalation.enabled || false, + levels: config.schedule.escalation.levels || [] + }); + } } // Load email configuration @@ -470,24 +549,18 @@ export function Alerts() { timeThreshold: timeThreshold() || 0, // Use rawOverridesConfig which is already properly formatted with disabled flags overrides: rawOverridesConfig(), - schedule: scheduleRef.getScheduleConfig ? scheduleRef.getScheduleConfig() : { - quietHours: { - enabled: false, - start: "22:00", - end: "08:00", - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - days: { - monday: true, - tuesday: true, - wednesday: true, - thursday: true, - friday: true, - saturday: false, - sunday: false - } - }, - cooldown: 5, - groupingWindow: 30 + schedule: { + quietHours: scheduleQuietHours(), + cooldown: scheduleCooldown().enabled ? scheduleCooldown().minutes : 0, + groupingWindow: scheduleGrouping().enabled && scheduleGrouping().window ? scheduleGrouping().window * 60 : 30, // Convert minutes to seconds + maxAlertsHour: scheduleCooldown().maxAlerts || 10, + escalation: scheduleEscalation(), + grouping: { + enabled: scheduleGrouping().enabled, + window: scheduleGrouping().window * 60, // Convert minutes to seconds + byNode: scheduleGrouping().byNode, + byGuest: scheduleGrouping().byGuest + } }, // Add missing required fields aggregation: { @@ -609,9 +682,16 @@ export function Alerts() { @@ -1161,49 +1241,28 @@ function DestinationsTab(props: DestinationsTabProps) { // History Tab - Alert history // Schedule Tab - Quiet hours, cooldown, and grouping interface ScheduleTabProps { - ref: ScheduleRef; hasUnsavedChanges: () => boolean; setHasUnsavedChanges: (value: boolean) => void; + quietHours: () => any; + setQuietHours: (value: any) => void; + cooldown: () => any; + setCooldown: (value: any) => void; + grouping: () => any; + setGrouping: (value: any) => void; + escalation: () => any; + setEscalation: (value: any) => void; } function ScheduleTab(props: ScheduleTabProps) { - const [quietHours, setQuietHours] = createSignal({ - enabled: false, - start: '22:00', - end: '08:00', - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC', - days: { - monday: true, - tuesday: true, - wednesday: true, - thursday: true, - friday: true, - saturday: false, - sunday: false - } as Record - }); - - const [cooldown, setCooldown] = createSignal({ - enabled: true, - minutes: 30, - maxAlerts: 3 - }); - - const [grouping, setGrouping] = createSignal({ - enabled: true, - window: 5, - byNode: true, - byGuest: false - }); - - const [escalation, setEscalation] = createSignal({ - enabled: false, - levels: [ - { after: 15, notify: 'email' }, - { after: 30, notify: 'webhook' }, - { after: 60, notify: 'all' } - ] - }); + // Use props instead of local state + const quietHours = props.quietHours; + const setQuietHours = props.setQuietHours; + const cooldown = props.cooldown; + const setCooldown = props.setCooldown; + const grouping = props.grouping; + const setGrouping = props.setGrouping; + const escalation = props.escalation; + const setEscalation = props.setEscalation; const timezones = [ 'UTC', @@ -1229,65 +1288,7 @@ function ScheduleTab(props: ScheduleTabProps) { { id: 'sunday', label: 'S', fullLabel: 'Sunday' } ]; - // Expose schedule config via ref - if (props.ref) { - props.ref.getScheduleConfig = () => ({ - quietHours: quietHours(), - cooldown: cooldown().enabled ? cooldown().minutes : 5, - groupingWindow: grouping().enabled && grouping().window ? grouping().window * 60 : 30, // Convert minutes to seconds - maxAlertsHour: cooldown().enabled && cooldown().maxAlerts ? cooldown().maxAlerts : 10, - escalation: escalation(), - grouping: grouping() - }); - - props.ref.setScheduleConfig = (config: ScheduleConfig) => { - if (config.quietHours) { - const qh = config.quietHours; - setQuietHours(prev => ({ - ...prev, - ...qh, - // Ensure timezone is preserved if not provided - timezone: qh.timezone || prev.timezone - })); - } - if (config.cooldown !== undefined) { - setCooldown({ - enabled: config.cooldown > 0, - minutes: config.cooldown, - maxAlerts: 3 - }); - } - if (config.groupingWindow !== undefined) { - const gw = config.groupingWindow; - setGrouping(prev => ({ - ...prev, - enabled: gw > 0, - window: Math.floor(gw / 60), // Convert seconds to minutes - // Preserve existing grouping preferences - byNode: config.grouping?.byNode !== undefined ? config.grouping.byNode : prev.byNode, - byGuest: config.grouping?.byGuest !== undefined ? config.grouping.byGuest : prev.byGuest - })); - } - if (config.maxAlertsHour !== undefined) { - setCooldown(prev => ({ ...prev, maxAlerts: config.maxAlertsHour! })); - } - if (config.escalation !== undefined) { - setEscalation(prev => ({ - ...prev, - enabled: config.escalation!.enabled - })); - } - if (config.grouping !== undefined) { - setGrouping(prev => ({ - ...prev, - enabled: config.grouping!.enabled, - window: config.grouping!.window, - byNode: config.grouping!.byNode ?? prev.byNode, - byGuest: config.grouping!.byGuest ?? prev.byGuest - })); - } - }; - } + // No longer need ref logic since we're using parent state return (
@@ -1683,7 +1684,7 @@ function ScheduleTab(props: ScheduleTabProps) {