Extract alert destinations state owner

This commit is contained in:
rcourtman
2026-03-22 12:38:33 +00:00
parent 683f2a8165
commit e8a62c9a12
7 changed files with 309 additions and 164 deletions
@@ -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
@@ -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
@@ -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);
});
});
@@ -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<AlertTab>;
}
function normalizeAppriseConfig(config: Partial<AppriseConfig> | 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<string | null>(null);
const [emailConfig, setEmailConfig] = createSignal<UIEmailConfig>(createDefaultEmailConfig());
const [appriseConfig, setAppriseConfig] = createSignal<UIAppriseConfig>(
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,
};
}
@@ -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<string | null>(null);
const [overrides, setOverrides] = createSignal<Override[]>([]);
const [rawOverridesConfig, setRawOverridesConfig] = createSignal<
Record<string, RawOverrideConfig>
>({});
const [emailConfig, setEmailConfig] = createSignal<UIEmailConfig>(createDefaultEmailConfig());
const [appriseConfig, setAppriseConfig] = createSignal<UIAppriseConfig>(
createDefaultAppriseConfig(),
);
const [scheduleQuietHours, setScheduleQuietHours] =
createSignal<QuietHoursConfig>(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<string, unknown> | 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,
@@ -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';",
);
@@ -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');