From e8a62c9a1238551cf7d7757f7d17d1dfc5931441 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 22 Mar 2026 12:38:33 +0000 Subject: [PATCH] Extract alert destinations state owner --- .../v6/internal/subsystems/alerts.md | 11 +- .../subsystems/frontend-primitives.md | 11 +- .../useAlertDestinationsState.test.tsx | 115 +++++++++++++ .../alerts/useAlertDestinationsState.ts | 157 +++++++++++++++++ .../alerts/useAlertsConfigurationState.ts | 160 ++---------------- .../pages/__tests__/Alerts.helpers.test.ts | 9 +- .../frontendResourceTypeBoundaries.test.ts | 10 +- 7 files changed, 309 insertions(+), 164 deletions(-) create mode 100644 frontend-modern/src/features/alerts/__tests__/useAlertDestinationsState.test.tsx create mode 100644 frontend-modern/src/features/alerts/useAlertDestinationsState.ts diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index d30296dccc..1edbe0d37c 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -204,10 +204,13 @@ override normalization now route through living inline in `frontend-modern/src/pages/Alerts.tsx`. The page shell owns navigation, activation chrome, and cross-surface routing; the configuration surface is now a shell that composes the destinations, schedule, and thresholds -tabs, while `frontend-modern/src/features/alerts/useAlertsConfigurationState.ts` -owns alert config load/save behavior, notification-config reloads, defaults, -override normalization, and derived resource mapping beneath that feature -boundary. +tabs. The canonical alert-policy runtime owner is now +`frontend-modern/src/features/alerts/useAlertsConfigurationState.ts`, while +notification destination reload and persistence now route through +`frontend-modern/src/features/alerts/useAlertDestinationsState.ts`. Future +config cleanup should extend the alert-policy hook or the destinations hook +based on which subsystem actually owns the behavior instead of letting the +broader configuration hook absorb both concerns again. Alert filter metadata and grouped header consumers must also preserve the canonical `agent` and `node` header boundary when reusing shared filter diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index b8c8a8d8c8..2bebb02a6d 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -417,12 +417,11 @@ The alerts page now also applies the same shell-versus-feature rule to configuration orchestration. `frontend-modern/src/pages/Alerts.tsx` is the page shell, while `frontend-modern/src/features/alerts/AlertsConfigurationSurface.tsx` is the feature shell. The canonical runtime owner is now -`frontend-modern/src/features/alerts/useAlertsConfigurationState.ts`, which -owns alert config load/save behavior, notification-config reloads, defaults, -derived resource mapping, and threshold-override normalization for the -destinations, schedule, and thresholds tabs. Future cleanup should extend that -feature hook or a narrower shared primitive, not move config control flow back -into the top-level page shell. +`frontend-modern/src/features/alerts/useAlertsConfigurationState.ts` for alert +policy state, while `frontend-modern/src/features/alerts/useAlertDestinationsState.ts` +owns notification destination reload and persistence. Future cleanup should +extend the alert-policy hook or the destinations hook based on the true owner, +not move config control flow back into the top-level page shell. The same rule now also covers cross-tab incident timelines: the shared runtime owner is `frontend-modern/src/features/alerts/useAlertIncidentTimelineState.ts`, while `frontend-modern/src/features/alerts/OverviewTab.tsx` and diff --git a/frontend-modern/src/features/alerts/__tests__/useAlertDestinationsState.test.tsx b/frontend-modern/src/features/alerts/__tests__/useAlertDestinationsState.test.tsx new file mode 100644 index 0000000000..8ae0cbab87 --- /dev/null +++ b/frontend-modern/src/features/alerts/__tests__/useAlertDestinationsState.test.tsx @@ -0,0 +1,115 @@ +import { renderHook } from '@solidjs/testing-library'; +import { createSignal } from 'solid-js'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { NotificationsAPI } from '@/api/notifications'; + +import { useAlertDestinationsState } from '../useAlertDestinationsState'; + +vi.mock('@/api/notifications', () => ({ + NotificationsAPI: { + getAppriseConfig: vi.fn(), + getEmailConfig: vi.fn(), + updateAppriseConfig: vi.fn(), + updateEmailConfig: vi.fn(), + }, +})); + +vi.mock('@/utils/logger', () => ({ + logger: { + error: vi.fn(), + }, +})); + +describe('useAlertDestinationsState', () => { + beforeEach(() => { + vi.mocked(NotificationsAPI.getEmailConfig).mockReset(); + vi.mocked(NotificationsAPI.getAppriseConfig).mockReset(); + vi.mocked(NotificationsAPI.updateEmailConfig).mockReset(); + vi.mocked(NotificationsAPI.updateAppriseConfig).mockReset(); + }); + + it('owns alert destinations reload and save behavior separately from alert policy config', async () => { + const [activeTab, setActiveTab] = createSignal<'overview' | 'destinations'>('overview'); + + vi.mocked(NotificationsAPI.getEmailConfig).mockResolvedValue({ + enabled: true, + provider: 'smtp', + server: 'smtp.example.com', + port: 587, + username: 'ops@example.com', + password: '', + from: 'pulse@example.com', + to: ['alerts@example.com'], + tls: true, + startTLS: true, + } as any); + vi.mocked(NotificationsAPI.getAppriseConfig).mockResolvedValue({ + enabled: true, + mode: 'cli', + targets: ['mailto://ops@example.com'], + cliPath: '/usr/local/bin/apprise', + timeoutSeconds: 20, + } as any); + vi.mocked(NotificationsAPI.updateEmailConfig).mockResolvedValue(undefined as any); + vi.mocked(NotificationsAPI.updateAppriseConfig).mockResolvedValue({ + enabled: true, + mode: 'http', + targets: ['https://notify.example.test'], + serverUrl: 'https://apprise.example.test', + configKey: 'prod', + apiKey: 'masked', + apiKeyHeader: 'X-API-KEY', + timeoutSeconds: 30, + skipTlsVerify: false, + } as any); + + const { result } = renderHook(() => useAlertDestinationsState({ activeTab })); + + await result.loadDestinations(); + expect(NotificationsAPI.getEmailConfig).toHaveBeenCalledTimes(1); + expect(NotificationsAPI.getAppriseConfig).toHaveBeenCalledTimes(1); + expect(result.emailConfig().server).toBe('smtp.example.com'); + expect(result.appriseConfig().targetsText).toContain('mailto://ops@example.com'); + + setActiveTab('destinations'); + await Promise.resolve(); + await Promise.resolve(); + expect(NotificationsAPI.getEmailConfig).toHaveBeenCalledTimes(2); + expect(NotificationsAPI.getAppriseConfig).toHaveBeenCalledTimes(2); + + setActiveTab('overview'); + await Promise.resolve(); + + result.setEmailConfig({ + ...result.emailConfig(), + server: 'smtp.internal', + }); + result.setAppriseConfig({ + ...result.appriseConfig(), + mode: 'http', + serverUrl: 'https://apprise.internal', + targetsText: 'https://notify.internal', + }); + + await result.saveDestinations(); + + expect(NotificationsAPI.updateEmailConfig).toHaveBeenCalledWith( + expect.objectContaining({ server: 'smtp.internal' }), + ); + expect(NotificationsAPI.updateAppriseConfig).toHaveBeenCalledWith( + expect.objectContaining({ + mode: 'http', + serverUrl: 'https://apprise.internal', + targets: ['https://notify.internal'], + }), + ); + expect(result.appriseConfig().mode).toBe('http'); + expect(result.appriseConfig().serverUrl).toBe('https://apprise.example.test'); + + result.resetDestinations(); + expect(result.destConfigLoadError()).toBeNull(); + expect(result.emailConfig().enabled).toBe(false); + expect(result.appriseConfig().enabled).toBe(false); + }); +}); diff --git a/frontend-modern/src/features/alerts/useAlertDestinationsState.ts b/frontend-modern/src/features/alerts/useAlertDestinationsState.ts new file mode 100644 index 0000000000..d07c708144 --- /dev/null +++ b/frontend-modern/src/features/alerts/useAlertDestinationsState.ts @@ -0,0 +1,157 @@ +import { createEffect, createSignal } from 'solid-js'; +import type { Accessor } from 'solid-js'; + +import type { AppriseConfig, EmailConfig } from '@/api/notifications'; +import { NotificationsAPI } from '@/api/notifications'; +import { getAlertDestinationsConfigLoadError } from '@/utils/alertDestinationsPresentation'; +import { logger } from '@/utils/logger'; + +import { + createDefaultAppriseConfig, + createDefaultEmailConfig, + formatAppriseTargets, + normalizeEmailConfigFromAPI, + parseAppriseTargets, +} from './helpers'; +import type { AlertTab, UIAppriseConfig, UIEmailConfig } from './types'; + +interface AlertDestinationsStateOptions { + activeTab: Accessor; +} + +function normalizeAppriseConfig(config: Partial | null | undefined): UIAppriseConfig { + return { + enabled: config?.enabled ?? false, + mode: config?.mode === 'http' ? 'http' : 'cli', + targetsText: formatAppriseTargets(config?.targets), + cliPath: config?.cliPath || 'apprise', + timeoutSeconds: + typeof config?.timeoutSeconds === 'number' && config.timeoutSeconds > 0 + ? config.timeoutSeconds + : 15, + serverUrl: config?.serverUrl || '', + configKey: config?.configKey || '', + apiKey: config?.apiKey || '', + apiKeyHeader: config?.apiKeyHeader || 'X-API-KEY', + skipTlsVerify: Boolean(config?.skipTlsVerify), + }; +} + +export function useAlertDestinationsState(options: AlertDestinationsStateOptions) { + const [isLoadingDestinations, setIsLoadingDestinations] = createSignal(false); + const [destConfigLoadError, setDestConfigLoadError] = createSignal(null); + const [emailConfig, setEmailConfig] = createSignal(createDefaultEmailConfig()); + const [appriseConfig, setAppriseConfig] = createSignal( + createDefaultAppriseConfig(), + ); + + let reloadVersion = 0; + let lastActiveTab: AlertTab | null = null; + + const resetDestinations = () => { + setDestConfigLoadError(null); + setEmailConfig(createDefaultEmailConfig()); + setAppriseConfig(createDefaultAppriseConfig()); + }; + + const loadDestinations = async (options: { indicateLoading?: boolean } = {}) => { + const indicateLoading = options.indicateLoading ?? false; + const thisVersion = ++reloadVersion; + + if (indicateLoading) { + setIsLoadingDestinations(true); + } + + const results = await Promise.allSettled([ + NotificationsAPI.getEmailConfig(), + NotificationsAPI.getAppriseConfig(), + ]); + + if (thisVersion !== reloadVersion) { + return; + } + + const [emailResult, appriseResult] = results; + + if (emailResult.status === 'fulfilled') { + setEmailConfig(normalizeEmailConfigFromAPI(emailResult.value)); + } + + if (appriseResult.status === 'fulfilled') { + setAppriseConfig(normalizeAppriseConfig(appriseResult.value)); + } + + const failures = results.filter( + (result): result is PromiseRejectedResult => result.status === 'rejected', + ); + + if (failures.length > 0) { + failures.forEach((result) => { + logger.error('Failed to load notification configuration:', result.reason); + }); + setDestConfigLoadError(getAlertDestinationsConfigLoadError()); + } else { + setDestConfigLoadError(null); + } + + if (indicateLoading) { + setIsLoadingDestinations(false); + } + }; + + const saveDestinations = async () => { + const currentEmailConfig = emailConfig(); + await NotificationsAPI.updateEmailConfig({ + enabled: currentEmailConfig.enabled, + provider: currentEmailConfig.provider, + server: currentEmailConfig.server, + port: currentEmailConfig.port, + username: currentEmailConfig.username, + password: currentEmailConfig.password, + from: currentEmailConfig.from, + to: currentEmailConfig.to, + tls: currentEmailConfig.tls, + startTLS: currentEmailConfig.startTLS, + } as EmailConfig); + + const currentAppriseConfig = appriseConfig(); + const updatedApprise = await NotificationsAPI.updateAppriseConfig({ + enabled: currentAppriseConfig.enabled, + mode: currentAppriseConfig.mode, + targets: parseAppriseTargets(currentAppriseConfig.targetsText), + cliPath: currentAppriseConfig.cliPath, + timeoutSeconds: currentAppriseConfig.timeoutSeconds, + serverUrl: currentAppriseConfig.serverUrl, + configKey: currentAppriseConfig.configKey, + apiKey: currentAppriseConfig.apiKey, + apiKeyHeader: currentAppriseConfig.apiKeyHeader, + skipTlsVerify: currentAppriseConfig.skipTlsVerify, + } as AppriseConfig); + + setAppriseConfig(normalizeAppriseConfig(updatedApprise)); + }; + + createEffect(() => { + const current = options.activeTab(); + const previous = lastActiveTab; + lastActiveTab = current; + + if (current !== 'destinations' || previous === null) { + return; + } + + void loadDestinations({ indicateLoading: true }); + }); + + return { + isLoadingDestinations, + destConfigLoadError, + emailConfig, + setEmailConfig, + appriseConfig, + setAppriseConfig, + resetDestinations, + loadDestinations, + saveDestinations, + }; +} diff --git a/frontend-modern/src/features/alerts/useAlertsConfigurationState.ts b/frontend-modern/src/features/alerts/useAlertsConfigurationState.ts index 73b0dc4550..1e609cf081 100644 --- a/frontend-modern/src/features/alerts/useAlertsConfigurationState.ts +++ b/frontend-modern/src/features/alerts/useAlertsConfigurationState.ts @@ -2,11 +2,9 @@ import { createEffect, createMemo, createSignal, onCleanup, onMount } from 'soli import type { Accessor } from 'solid-js'; import { AlertsAPI } from '@/api/alerts'; -import { NotificationsAPI } from '@/api/notifications'; import { eventBus } from '@/stores/events'; import { notificationStore } from '@/stores/notifications'; import type { Alert, PBSInstance, PMGInstance } from '@/types/api'; -import type { AppriseConfig, EmailConfig } from '@/api/notifications'; import type { ActivationState, BackupAlertConfig, @@ -19,7 +17,6 @@ import { getAlertConfigReloadFailure, getAlertConfigSaveSuccess, } from '@/utils/alertConfigPresentation'; -import { getAlertDestinationsConfigLoadError } from '@/utils/alertDestinationsPresentation'; import { getActionableAgentIdFromResource, hasAgentFacet } from '@/utils/agentResources'; import { isAppContainerDiscoveryResourceType } from '@/utils/discoveryTarget'; import { logger } from '@/utils/logger'; @@ -27,9 +24,7 @@ import { pbsInstanceFromResource, pmgInstanceFromResource } from '@/utils/resour import { clampMaxAlertsPerHour, - createDefaultAppriseConfig, createDefaultCooldown, - createDefaultEmailConfig, createDefaultEscalation, createDefaultGrouping, createDefaultQuietHours, @@ -37,15 +32,13 @@ import { DEFAULT_DELAY_SECONDS, extractTriggerValues, fallbackMaxAlertsPerHour, - formatAppriseTargets, getAlertResourceDisplayLabel, getTriggerValue, guessNumericId, - normalizeEmailConfigFromAPI, normalizeMetricDelayMap, - parseAppriseTargets, platformData, } from './helpers'; +import { useAlertDestinationsState } from './useAlertDestinationsState'; import type { AlertTab, CooldownConfig, @@ -54,8 +47,6 @@ import type { GroupingConfig, Override, QuietHoursConfig, - UIAppriseConfig, - UIEmailConfig, } from './types'; import { GROUPING_WINDOW_DEFAULT_SECONDS, clampCooldownMinutes } from './types'; @@ -138,16 +129,10 @@ export const FACTORY_BACKUP_DEFAULTS: BackupAlertConfig = { export function useAlertsConfigurationState(props: AlertsConfigurationSurfaceProps) { const [isReloadingConfig, setIsReloadingConfig] = createSignal(false); - const [isLoadingDestinations, setIsLoadingDestinations] = createSignal(false); - const [destConfigLoadError, setDestConfigLoadError] = createSignal(null); const [overrides, setOverrides] = createSignal([]); const [rawOverridesConfig, setRawOverridesConfig] = createSignal< Record >({}); - const [emailConfig, setEmailConfig] = createSignal(createDefaultEmailConfig()); - const [appriseConfig, setAppriseConfig] = createSignal( - createDefaultAppriseConfig(), - ); const [scheduleQuietHours, setScheduleQuietHours] = createSignal(createDefaultQuietHours()); const [scheduleCooldown, setScheduleCooldown] = @@ -236,6 +221,7 @@ export function useAlertsConfigurationState(props: AlertsConfigurationSurfacePro const [disableAllPBSOffline, setDisableAllPBSOffline] = createSignal(false); const [disableAllPMGOffline, setDisableAllPMGOffline] = createSignal(false); const [disableAllDockerHostsOffline, setDisableAllDockerHostsOffline] = createSignal(false); + const destinationsState = useAlertDestinationsState({ activeTab: props.activeTab }); const pd = platformData; const asRecord = (value: unknown): Record | undefined => @@ -645,7 +631,7 @@ export function useAlertsConfigurationState(props: AlertsConfigurationSurfacePro const loadAlertConfiguration = async (options: { notify?: boolean } = {}) => { setIsReloadingConfig(true); props.setHasUnsavedChanges(false); - setDestConfigLoadError(null); + destinationsState.resetDestinations(); setGuestDefaults({ ...FACTORY_GUEST_DEFAULTS }); setGuestDisableConnectivity(false); @@ -674,8 +660,6 @@ export function useAlertsConfigurationState(props: AlertsConfigurationSurfacePro setScheduleGrouping(createDefaultGrouping()); setScheduleEscalation(createDefaultEscalation()); setNotifyOnResolve(createDefaultResolveNotifications()); - setEmailConfig(createDefaultEmailConfig()); - setAppriseConfig(createDefaultAppriseConfig()); setBackupDefaults({ ...FACTORY_BACKUP_DEFAULTS }); setSnapshotDefaults({ ...FACTORY_SNAPSHOT_DEFAULTS }); @@ -971,42 +955,13 @@ export function useAlertsConfigurationState(props: AlertsConfigurationSurfacePro } } - try { - const emailConfigData = await NotificationsAPI.getEmailConfig(); - setEmailConfig(normalizeEmailConfigFromAPI(emailConfigData)); - } catch (emailError) { - logger.error('Failed to load email configuration:', emailError); - setDestConfigLoadError(getAlertDestinationsConfigLoadError()); - } - - try { - const appriseData = await NotificationsAPI.getAppriseConfig(); - setAppriseConfig({ - enabled: appriseData.enabled ?? false, - mode: appriseData.mode === 'http' ? 'http' : 'cli', - targetsText: formatAppriseTargets(appriseData.targets), - cliPath: appriseData.cliPath || 'apprise', - timeoutSeconds: - typeof appriseData.timeoutSeconds === 'number' && appriseData.timeoutSeconds > 0 - ? appriseData.timeoutSeconds - : 15, - serverUrl: appriseData.serverUrl || '', - configKey: appriseData.configKey || '', - apiKey: appriseData.apiKey || '', - apiKeyHeader: appriseData.apiKeyHeader || 'X-API-KEY', - skipTlsVerify: Boolean(appriseData.skipTlsVerify), - }); - } catch (appriseError) { - logger.error('Failed to load Apprise configuration:', appriseError); - setDestConfigLoadError(getAlertDestinationsConfigLoadError()); - } + await destinationsState.loadDestinations(); if (options.notify) { notificationStore.success(getAlertConfigDiscardedSuccess()); } } catch (error) { logger.error('Failed to load alert configuration:', error); - setDestConfigLoadError(getAlertDestinationsConfigLoadError()); if (options.notify) { notificationStore.error(getAlertConfigReloadFailure()); } @@ -1193,48 +1148,7 @@ export function useAlertsConfigurationState(props: AlertsConfigurationSurfacePro await AlertsAPI.updateConfig(alertConfig); - const currentEmailConfig = emailConfig(); - await NotificationsAPI.updateEmailConfig({ - enabled: currentEmailConfig.enabled, - provider: currentEmailConfig.provider, - server: currentEmailConfig.server, - port: currentEmailConfig.port, - username: currentEmailConfig.username, - password: currentEmailConfig.password, - from: currentEmailConfig.from, - to: currentEmailConfig.to, - tls: currentEmailConfig.tls, - startTLS: currentEmailConfig.startTLS, - } as EmailConfig); - - const currentAppriseConfig = appriseConfig(); - const updatedApprise = await NotificationsAPI.updateAppriseConfig({ - enabled: currentAppriseConfig.enabled, - mode: currentAppriseConfig.mode, - targets: parseAppriseTargets(currentAppriseConfig.targetsText), - cliPath: currentAppriseConfig.cliPath, - timeoutSeconds: currentAppriseConfig.timeoutSeconds, - serverUrl: currentAppriseConfig.serverUrl, - configKey: currentAppriseConfig.configKey, - apiKey: currentAppriseConfig.apiKey, - apiKeyHeader: currentAppriseConfig.apiKeyHeader, - skipTlsVerify: currentAppriseConfig.skipTlsVerify, - } as AppriseConfig); - setAppriseConfig({ - enabled: updatedApprise.enabled ?? false, - mode: updatedApprise.mode === 'http' ? 'http' : 'cli', - targetsText: formatAppriseTargets(updatedApprise.targets), - cliPath: updatedApprise.cliPath || 'apprise', - timeoutSeconds: - typeof updatedApprise.timeoutSeconds === 'number' && updatedApprise.timeoutSeconds > 0 - ? updatedApprise.timeoutSeconds - : 15, - serverUrl: updatedApprise.serverUrl || '', - configKey: updatedApprise.configKey || '', - apiKey: updatedApprise.apiKey || '', - apiKeyHeader: updatedApprise.apiKeyHeader || 'X-API-KEY', - skipTlsVerify: Boolean(updatedApprise.skipTlsVerify), - }); + await destinationsState.saveDestinations(); props.setHasUnsavedChanges(false); notificationStore.success(getAlertConfigSaveSuccess()); }; @@ -1249,70 +1163,18 @@ export function useAlertsConfigurationState(props: AlertsConfigurationSurfacePro }); }); - let destReloadVersion = 0; - createEffect(() => { - if (props.activeTab() !== 'destinations') { - return; - } - - const thisVersion = ++destReloadVersion; - setIsLoadingDestinations(true); - - const emailPromise = NotificationsAPI.getEmailConfig().then((emailConfigData) => { - if (thisVersion === destReloadVersion) { - setEmailConfig(normalizeEmailConfigFromAPI(emailConfigData)); - } - }); - - const apprisePromise = NotificationsAPI.getAppriseConfig().then((appriseData) => { - if (thisVersion === destReloadVersion) { - setAppriseConfig({ - enabled: appriseData.enabled ?? false, - mode: appriseData.mode === 'http' ? 'http' : 'cli', - targetsText: formatAppriseTargets(appriseData.targets), - cliPath: appriseData.cliPath || 'apprise', - timeoutSeconds: - typeof appriseData.timeoutSeconds === 'number' && appriseData.timeoutSeconds > 0 - ? appriseData.timeoutSeconds - : 15, - serverUrl: appriseData.serverUrl || '', - configKey: appriseData.configKey || '', - apiKey: appriseData.apiKey || '', - apiKeyHeader: appriseData.apiKeyHeader || 'X-API-KEY', - skipTlsVerify: Boolean(appriseData.skipTlsVerify), - }); - } - }); - - void Promise.allSettled([emailPromise, apprisePromise]).then((results) => { - if (thisVersion !== destReloadVersion) return; - const failed = results.some((result) => result.status === 'rejected'); - if (failed) { - results - .filter((result): result is PromiseRejectedResult => result.status === 'rejected') - .forEach((result) => { - logger.error('Failed to reload notification configuration:', result.reason); - }); - setDestConfigLoadError(getAlertDestinationsConfigLoadError()); - } else { - setDestConfigLoadError(null); - } - setIsLoadingDestinations(false); - }); - }); - return { isReloadingConfig, - isLoadingDestinations, - destConfigLoadError, + isLoadingDestinations: destinationsState.isLoadingDestinations, + destConfigLoadError: destinationsState.destConfigLoadError, overrides, setOverrides, rawOverridesConfig, setRawOverridesConfig, - emailConfig, - setEmailConfig, - appriseConfig, - setAppriseConfig, + emailConfig: destinationsState.emailConfig, + setEmailConfig: destinationsState.setEmailConfig, + appriseConfig: destinationsState.appriseConfig, + setAppriseConfig: destinationsState.setAppriseConfig, scheduleQuietHours, setScheduleQuietHours, scheduleCooldown, diff --git a/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts b/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts index df6a6c0383..0eda5bcea8 100644 --- a/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts +++ b/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import alertsPageSource from '@/pages/Alerts.tsx?raw'; import alertsConfigurationSurfaceSource from '@/features/alerts/AlertsConfigurationSurface.tsx?raw'; import alertsConfigurationStateSource from '@/features/alerts/useAlertsConfigurationState.ts?raw'; +import alertDestinationsStateSource from '@/features/alerts/useAlertDestinationsState.ts?raw'; import alertIncidentTimelineStateSource from '@/features/alerts/useAlertIncidentTimelineState.ts?raw'; import alertDestinationsTabSource from '@/features/alerts/tabs/DestinationsTab.tsx?raw'; import alertHistoryTabSource from '@/features/alerts/tabs/HistoryTab.tsx?raw'; @@ -185,9 +186,13 @@ describe('tab path helpers', () => { expect(alertsConfigurationSurfaceSource).not.toContain("eventBus.on('org_switched'"); expect(alertsConfigurationStateSource).toContain('export function useAlertsConfigurationState'); expect(alertsConfigurationStateSource).toContain('AlertsAPI.getConfig'); - expect(alertsConfigurationStateSource).toContain('NotificationsAPI.getEmailConfig'); - expect(alertsConfigurationStateSource).toContain('NotificationsAPI.updateEmailConfig'); + expect(alertsConfigurationStateSource).toContain('useAlertDestinationsState'); + expect(alertsConfigurationStateSource).not.toContain('NotificationsAPI.getEmailConfig'); + expect(alertsConfigurationStateSource).not.toContain('NotificationsAPI.updateEmailConfig'); expect(alertsConfigurationStateSource).toContain("eventBus.on('org_switched'"); + expect(alertDestinationsStateSource).toContain('export function useAlertDestinationsState'); + expect(alertDestinationsStateSource).toContain('NotificationsAPI.getEmailConfig'); + expect(alertDestinationsStateSource).toContain('NotificationsAPI.updateEmailConfig'); expect(alertsPageSource).toContain( "import { HistoryTab } from '@/features/alerts/tabs/HistoryTab';", ); diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index c1b54725a8..6c2afe9c5d 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -326,6 +326,7 @@ import alertWebhookPresentationSource from '@/utils/alertWebhookPresentation.ts? import alertOverviewTabSource from '@/features/alerts/OverviewTab.tsx?raw'; import alertsConfigurationSurfaceSource from '@/features/alerts/AlertsConfigurationSurface.tsx?raw'; import alertsConfigurationStateSource from '@/features/alerts/useAlertsConfigurationState.ts?raw'; +import alertDestinationsStateSource from '@/features/alerts/useAlertDestinationsState.ts?raw'; import alertIncidentTimelineStateSource from '@/features/alerts/useAlertIncidentTimelineState.ts?raw'; import alertDestinationsTabSource from '@/features/alerts/tabs/DestinationsTab.tsx?raw'; import alertHistoryTabSource from '@/features/alerts/tabs/HistoryTab.tsx?raw'; @@ -2764,11 +2765,14 @@ describe('frontend resource type boundaries', () => { expect(alertsPageSource).not.toContain('getAlertDestinationsConfigLoadError'); expect(alertsConfigurationSurfaceSource).toContain('useAlertsConfigurationState'); expect(alertsConfigurationSurfaceSource).not.toContain('AlertsAPI.getConfig'); - expect(alertsConfigurationStateSource).toContain('getAlertDestinationsConfigLoadError'); expect(alertsConfigurationStateSource).toContain('AlertsAPI.getConfig'); - expect(alertsConfigurationStateSource).toContain('NotificationsAPI.getEmailConfig'); - expect(alertsConfigurationStateSource).toContain('NotificationsAPI.updateEmailConfig'); + expect(alertsConfigurationStateSource).toContain('useAlertDestinationsState'); + expect(alertsConfigurationStateSource).not.toContain('NotificationsAPI.getEmailConfig'); + expect(alertsConfigurationStateSource).not.toContain('NotificationsAPI.updateEmailConfig'); expect(alertsConfigurationStateSource).toContain("eventBus.on('org_switched'"); + expect(alertDestinationsStateSource).toContain('getAlertDestinationsConfigLoadError'); + expect(alertDestinationsStateSource).toContain('NotificationsAPI.getEmailConfig'); + expect(alertDestinationsStateSource).toContain('NotificationsAPI.updateEmailConfig'); expect(alertDestinationsTabSource).toContain('getAlertDestinationsWebhookLoadError'); expect(alertDestinationsTabSource).toContain('getAlertDestinationsLoadErrorBanner'); expect(alertDestinationsTabSource).toContain('getAlertDestinationsAppriseTargetsHelp');