diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index 0afb0e3b3..9a6f064e3 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -250,13 +250,16 @@ serialization, override normalization plus resource-backed override projection, and `frontend-modern/src/features/alerts/useAlertDestinationsState.ts` for notification destination reload and persistence. +`frontend-modern/src/features/alerts/useAlertWebhookDestinationsState.ts` now +owns webhook load/mutate/test flow, `frontend-modern/src/features/alerts/useAlertDestinationsTabState.ts` now owns -webhook load/mutate/test flow plus destination test actions, while +destination test actions plus retry orchestration around that webhook runtime, +while `frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx` stays the destinations render shell. Future config cleanup should extend the config transport hook, the config model, the override-projection hook, or the destinations runtime hook based on which subsystem actually owns the behavior -instead of letting the broader configuration hook absorb all three concerns +instead of letting the broader configuration hook absorb all four concerns again. The email destination provider picker now follows that same split: `frontend-modern/src/components/Alerts/useEmailProviderSelectState.ts` owns diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 0df77960d..348f555bc 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -451,8 +451,10 @@ serialization, `frontend-modern/src/features/alerts/useAlertOverridesState.ts` for override projection and thresholds-facing resource selectors, and `frontend-modern/src/features/alerts/useAlertDestinationsState.ts` for notification destination reload and persistence. +`frontend-modern/src/features/alerts/useAlertWebhookDestinationsState.ts` now +owns webhook runtime, and `frontend-modern/src/features/alerts/useAlertDestinationsTabState.ts` now owns -webhook runtime and destination test actions while +destination test actions plus retry orchestration while `frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx` stays the render shell. Future cleanup should extend the transport hook, config model, override hook, or destinations runtime hook based on the true owner, not move diff --git a/frontend-modern/src/features/alerts/__tests__/useAlertDestinationsTabState.test.tsx b/frontend-modern/src/features/alerts/__tests__/useAlertDestinationsTabState.test.tsx index ab0926f41..bd4329c0c 100644 --- a/frontend-modern/src/features/alerts/__tests__/useAlertDestinationsTabState.test.tsx +++ b/frontend-modern/src/features/alerts/__tests__/useAlertDestinationsTabState.test.tsx @@ -100,25 +100,6 @@ describe('useAlertDestinationsTabState', () => { ] as never); vi.mocked(NotificationsAPI.testNotification).mockResolvedValue({ success: true } as never); vi.mocked(NotificationsAPI.testWebhook).mockResolvedValue({ success: true } as never); - vi.mocked(NotificationsAPI.createWebhook).mockResolvedValue({ - enabled: true, - headers: {}, - id: 'hook-2', - method: 'POST', - name: 'Pager', - service: 'slack', - url: 'https://hooks.example.test/pager', - } as never); - vi.mocked(NotificationsAPI.updateWebhook).mockResolvedValue({ - enabled: false, - headers: {}, - id: 'hook-2', - method: 'POST', - name: 'Pager Updated', - service: 'slack', - url: 'https://hooks.example.test/pager', - } as never); - vi.mocked(NotificationsAPI.deleteWebhook).mockResolvedValue({ success: true } as never); const { result } = renderHook(() => useAlertDestinationsTabState({ @@ -153,37 +134,9 @@ describe('useAlertDestinationsTabState', () => { }), ); - await result.addWebhook({ - enabled: true, - headers: {}, - method: 'POST', - name: 'Pager', - service: 'slack', - url: 'https://hooks.example.test/pager', - }); - expect(result.webhooks().map((hook) => hook.id)).toEqual(['hook-1', 'hook-2']); - - await result.updateWebhook({ - enabled: true, - headers: {}, - id: 'hook-2', - method: 'POST', - name: 'Pager', - service: 'slack', - url: 'https://hooks.example.test/pager', - }); - expect(result.webhooks().find((hook) => hook.id === 'hook-2')).toEqual( - expect.objectContaining({ enabled: false, name: 'Pager Updated' }), - ); - - await result.testWebhook('hook-2'); - expect(NotificationsAPI.testNotification).toHaveBeenCalledWith({ - type: 'webhook', - webhookId: 'hook-2', - }); - - await result.deleteWebhook('hook-1'); - expect(result.webhooks().map((hook) => hook.id)).toEqual(['hook-2']); + expect(result.webhooks()).toEqual([ + expect.objectContaining({ id: 'hook-1', service: 'generic' }), + ]); result.updateApprise({ mode: 'http', serverUrl: 'https://apprise.internal' }); expect(result.appriseState()).toEqual( @@ -193,7 +146,7 @@ describe('useAlertDestinationsTabState', () => { result.handleRetry(); expect(onRetryLoad).toHaveBeenCalledTimes(1); await waitFor(() => expect(NotificationsAPI.getWebhooks).toHaveBeenCalledTimes(2)); - expect(notificationStore.success).toHaveBeenCalled(); + expect(notificationStore.success).toHaveBeenCalledTimes(2); expect(showErrorWithDetail).not.toHaveBeenCalled(); }); }); diff --git a/frontend-modern/src/features/alerts/__tests__/useAlertWebhookDestinationsState.test.tsx b/frontend-modern/src/features/alerts/__tests__/useAlertWebhookDestinationsState.test.tsx new file mode 100644 index 000000000..0cde2a53b --- /dev/null +++ b/frontend-modern/src/features/alerts/__tests__/useAlertWebhookDestinationsState.test.tsx @@ -0,0 +1,127 @@ +import { renderHook, waitFor } from '@solidjs/testing-library'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { NotificationsAPI } from '@/api/notifications'; +import { notificationStore } from '@/stores/notifications'; +import { showErrorWithDetail } from '@/utils/toast'; + +import { useAlertWebhookDestinationsState } from '../useAlertWebhookDestinationsState'; + +vi.mock('@/api/notifications', () => ({ + NotificationsAPI: { + createWebhook: vi.fn(), + deleteWebhook: vi.fn(), + getWebhooks: vi.fn(), + testNotification: vi.fn(), + testWebhook: vi.fn(), + updateWebhook: vi.fn(), + }, +})); + +vi.mock('@/stores/notifications', () => ({ + notificationStore: { + error: vi.fn(), + success: vi.fn(), + }, +})); + +vi.mock('@/utils/logger', () => ({ + logger: { + error: vi.fn(), + }, +})); + +vi.mock('@/utils/toast', () => ({ + showErrorWithDetail: vi.fn(), +})); + +describe('useAlertWebhookDestinationsState', () => { + beforeEach(() => { + vi.mocked(NotificationsAPI.createWebhook).mockReset(); + vi.mocked(NotificationsAPI.deleteWebhook).mockReset(); + vi.mocked(NotificationsAPI.getWebhooks).mockReset(); + vi.mocked(NotificationsAPI.testNotification).mockReset(); + vi.mocked(NotificationsAPI.testWebhook).mockReset(); + vi.mocked(NotificationsAPI.updateWebhook).mockReset(); + vi.mocked(notificationStore.error).mockReset(); + vi.mocked(notificationStore.success).mockReset(); + vi.mocked(showErrorWithDetail).mockReset(); + }); + + it('owns webhook load, mutation, and test runtime for alert destinations', async () => { + vi.mocked(NotificationsAPI.getWebhooks).mockResolvedValue([ + { + enabled: true, + headers: {}, + id: 'hook-1', + method: 'POST', + name: 'Ops', + url: 'https://hooks.example.test/ops', + }, + ] as never); + vi.mocked(NotificationsAPI.testNotification).mockResolvedValue({ success: true } as never); + vi.mocked(NotificationsAPI.createWebhook).mockResolvedValue({ + enabled: true, + headers: {}, + id: 'hook-2', + method: 'POST', + name: 'Pager', + service: 'slack', + url: 'https://hooks.example.test/pager', + } as never); + vi.mocked(NotificationsAPI.updateWebhook).mockResolvedValue({ + enabled: false, + headers: {}, + id: 'hook-2', + method: 'POST', + name: 'Pager Updated', + service: 'slack', + url: 'https://hooks.example.test/pager', + } as never); + vi.mocked(NotificationsAPI.deleteWebhook).mockResolvedValue({ success: true } as never); + + const { result } = renderHook(() => useAlertWebhookDestinationsState()); + + await waitFor(() => expect(NotificationsAPI.getWebhooks).toHaveBeenCalledTimes(1)); + expect(result.webhooks()).toEqual([ + expect.objectContaining({ id: 'hook-1', service: 'generic' }), + ]); + + await result.addWebhook({ + enabled: true, + headers: {}, + method: 'POST', + name: 'Pager', + service: 'slack', + url: 'https://hooks.example.test/pager', + }); + expect(result.webhooks().map((hook) => hook.id)).toEqual(['hook-1', 'hook-2']); + + await result.updateWebhook({ + enabled: true, + headers: {}, + id: 'hook-2', + method: 'POST', + name: 'Pager', + service: 'slack', + url: 'https://hooks.example.test/pager', + }); + expect(result.webhooks().find((hook) => hook.id === 'hook-2')).toEqual( + expect.objectContaining({ enabled: false, name: 'Pager Updated' }), + ); + + await result.testWebhook('hook-2'); + expect(NotificationsAPI.testNotification).toHaveBeenCalledWith({ + type: 'webhook', + webhookId: 'hook-2', + }); + + await result.deleteWebhook('hook-1'); + expect(result.webhooks().map((hook) => hook.id)).toEqual(['hook-2']); + + await result.loadWebhooks(); + expect(NotificationsAPI.getWebhooks).toHaveBeenCalledTimes(2); + expect(notificationStore.success).toHaveBeenCalled(); + expect(showErrorWithDetail).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend-modern/src/features/alerts/useAlertDestinationsTabState.ts b/frontend-modern/src/features/alerts/useAlertDestinationsTabState.ts index db45c123f..e50b3159f 100644 --- a/frontend-modern/src/features/alerts/useAlertDestinationsTabState.ts +++ b/frontend-modern/src/features/alerts/useAlertDestinationsTabState.ts @@ -1,6 +1,6 @@ -import { createMemo, createSignal, onMount, type Accessor } from 'solid-js'; +import { createMemo, createSignal, type Accessor } from 'solid-js'; -import { NotificationsAPI, type AppriseConfig, type Webhook } from '@/api/notifications'; +import { NotificationsAPI, type AppriseConfig } from '@/api/notifications'; import { notificationStore } from '@/stores/notifications'; import { logger } from '@/utils/logger'; import { showErrorWithDetail } from '@/utils/toast'; @@ -10,17 +10,11 @@ import { getAlertDestinationsAppriseValidationError, getAlertDestinationsEmailTestFailure, getAlertDestinationsEmailTestSuccess, - getAlertDestinationsWebhookLoadError, } from '@/utils/alertDestinationsPresentation'; -import { - getAlertWebhookMutationFailure, - getAlertWebhookMutationSuccess, - getAlertWebhookTestFailure, - getAlertWebhookTestSuccess, -} from '@/utils/alertWebhookPresentation'; import { parseAppriseTargets } from './helpers'; import type { UIAppriseConfig, UIEmailConfig } from './types'; +import { useAlertWebhookDestinationsState } from './useAlertWebhookDestinationsState'; export interface AlertDestinationsTabStateProps { emailConfig: Accessor; @@ -32,23 +26,15 @@ export interface AlertDestinationsTabStateProps { onRetryLoad: () => void; } -const normalizeWebhook = (webhook: Webhook): Webhook => ({ - ...webhook, - service: webhook.service || 'generic', -}); - export function useAlertDestinationsTabState(props: AlertDestinationsTabStateProps) { - const [webhooks, setWebhooks] = createSignal([]); - const [webhookLoadError, setWebhookLoadError] = createSignal(null); - const [isLoadingWebhooks, setIsLoadingWebhooks] = createSignal(true); const [testingEmail, setTestingEmail] = createSignal(false); const [testingApprise, setTestingApprise] = createSignal(false); - const [testingWebhook, setTestingWebhook] = createSignal(null); + const webhookState = useAlertWebhookDestinationsState(); const isLoading = createMemo( - () => props.isLoadingDestinations() || isLoadingWebhooks() || props.isRetrying(), + () => props.isLoadingDestinations() || webhookState.isLoadingWebhooks() || props.isRetrying(), ); - const hasLoadError = createMemo(() => props.configLoadError() || webhookLoadError()); + const hasLoadError = createMemo(() => props.configLoadError() || webhookState.webhookLoadError()); const appriseState = createMemo(() => props.appriseConfig()); const updateApprise = (partial: Partial) => { @@ -73,24 +59,6 @@ export function useAlertDestinationsTabState(props: AlertDestinationsTabStatePro }; }; - const loadWebhooks = async () => { - setWebhookLoadError(null); - setIsLoadingWebhooks(true); - try { - const hooks = await NotificationsAPI.getWebhooks(); - setWebhooks(hooks.map(normalizeWebhook)); - } catch (error) { - logger.error('Failed to load webhooks:', error); - setWebhookLoadError(getAlertDestinationsWebhookLoadError()); - } finally { - setIsLoadingWebhooks(false); - } - }; - - onMount(() => { - void loadWebhooks(); - }); - const testEmailConfig = async () => { setTestingEmail(true); try { @@ -143,88 +111,21 @@ export function useAlertDestinationsTabState(props: AlertDestinationsTabStatePro } }; - const testWebhook = async (webhookId: string, webhookData?: Omit) => { - setTestingWebhook(webhookId); - try { - if (webhookData) { - await NotificationsAPI.testWebhook(webhookData); - } else { - await NotificationsAPI.testNotification({ type: 'webhook', webhookId }); - } - notificationStore.success(getAlertWebhookTestSuccess()); - } catch (error) { - const message = error instanceof Error ? error.message : getAlertWebhookTestFailure(); - const detail = (error as Error & { detail?: string })?.detail; - showErrorWithDetail(message, detail); - } finally { - setTestingWebhook(null); - } - }; - - const addWebhook = async (webhook: Omit) => { - try { - const created = await NotificationsAPI.createWebhook(webhook); - setWebhooks((current) => [...current, normalizeWebhook(created)]); - notificationStore.success(getAlertWebhookMutationSuccess('add')); - } catch (error) { - logger.error('Failed to add webhook:', error); - notificationStore.error( - error instanceof Error ? error.message : getAlertWebhookMutationFailure('add'), - ); - } - }; - - const updateWebhook = async (webhook: Webhook) => { - try { - const updated = await NotificationsAPI.updateWebhook(webhook.id, webhook); - setWebhooks((current) => - current.map((entry) => (entry.id === webhook.id ? normalizeWebhook(updated) : entry)), - ); - notificationStore.success(getAlertWebhookMutationSuccess('update')); - } catch (error) { - logger.error('Failed to update webhook:', error); - notificationStore.error( - error instanceof Error ? error.message : getAlertWebhookMutationFailure('update'), - ); - } - }; - - const deleteWebhook = async (id: string) => { - try { - await NotificationsAPI.deleteWebhook(id); - setWebhooks((current) => current.filter((entry) => entry.id !== id)); - notificationStore.success(getAlertWebhookMutationSuccess('delete')); - } catch (error) { - logger.error('Failed to delete webhook:', error); - notificationStore.error( - error instanceof Error ? error.message : getAlertWebhookMutationFailure('delete'), - ); - } - }; - const handleRetry = () => { props.onRetryLoad(); - void loadWebhooks(); + void webhookState.loadWebhooks(); }; return { - addWebhook, appriseState, - deleteWebhook, handleRetry, hasLoadError, isLoading, - isLoadingWebhooks, - loadWebhooks, testApprise, testEmailConfig, - testWebhook, testingApprise, testingEmail, - testingWebhook, updateApprise, - updateWebhook, - webhookLoadError, - webhooks, + ...webhookState, }; } diff --git a/frontend-modern/src/features/alerts/useAlertWebhookDestinationsState.ts b/frontend-modern/src/features/alerts/useAlertWebhookDestinationsState.ts new file mode 100644 index 000000000..5064c687b --- /dev/null +++ b/frontend-modern/src/features/alerts/useAlertWebhookDestinationsState.ts @@ -0,0 +1,114 @@ +import { createSignal, onMount } from 'solid-js'; + +import { NotificationsAPI, type Webhook } from '@/api/notifications'; +import { notificationStore } from '@/stores/notifications'; +import { logger } from '@/utils/logger'; +import { showErrorWithDetail } from '@/utils/toast'; +import { getAlertDestinationsWebhookLoadError } from '@/utils/alertDestinationsPresentation'; +import { + getAlertWebhookMutationFailure, + getAlertWebhookMutationSuccess, + getAlertWebhookTestFailure, + getAlertWebhookTestSuccess, +} from '@/utils/alertWebhookPresentation'; + +const normalizeWebhook = (webhook: Webhook): Webhook => ({ + ...webhook, + service: webhook.service || 'generic', +}); + +export function useAlertWebhookDestinationsState() { + const [webhooks, setWebhooks] = createSignal([]); + const [webhookLoadError, setWebhookLoadError] = createSignal(null); + const [isLoadingWebhooks, setIsLoadingWebhooks] = createSignal(true); + const [testingWebhook, setTestingWebhook] = createSignal(null); + + const loadWebhooks = async () => { + setWebhookLoadError(null); + setIsLoadingWebhooks(true); + try { + const hooks = await NotificationsAPI.getWebhooks(); + setWebhooks(hooks.map(normalizeWebhook)); + } catch (error) { + logger.error('Failed to load webhooks:', error); + setWebhookLoadError(getAlertDestinationsWebhookLoadError()); + } finally { + setIsLoadingWebhooks(false); + } + }; + + onMount(() => { + void loadWebhooks(); + }); + + const testWebhook = async (webhookId: string, webhookData?: Omit) => { + setTestingWebhook(webhookId); + try { + if (webhookData) { + await NotificationsAPI.testWebhook(webhookData); + } else { + await NotificationsAPI.testNotification({ type: 'webhook', webhookId }); + } + notificationStore.success(getAlertWebhookTestSuccess()); + } catch (error) { + const message = error instanceof Error ? error.message : getAlertWebhookTestFailure(); + const detail = (error as Error & { detail?: string })?.detail; + showErrorWithDetail(message, detail); + } finally { + setTestingWebhook(null); + } + }; + + const addWebhook = async (webhook: Omit) => { + try { + const created = await NotificationsAPI.createWebhook(webhook); + setWebhooks((current) => [...current, normalizeWebhook(created)]); + notificationStore.success(getAlertWebhookMutationSuccess('add')); + } catch (error) { + logger.error('Failed to add webhook:', error); + notificationStore.error( + error instanceof Error ? error.message : getAlertWebhookMutationFailure('add'), + ); + } + }; + + const updateWebhook = async (webhook: Webhook) => { + try { + const updated = await NotificationsAPI.updateWebhook(webhook.id, webhook); + setWebhooks((current) => + current.map((entry) => (entry.id === webhook.id ? normalizeWebhook(updated) : entry)), + ); + notificationStore.success(getAlertWebhookMutationSuccess('update')); + } catch (error) { + logger.error('Failed to update webhook:', error); + notificationStore.error( + error instanceof Error ? error.message : getAlertWebhookMutationFailure('update'), + ); + } + }; + + const deleteWebhook = async (id: string) => { + try { + await NotificationsAPI.deleteWebhook(id); + setWebhooks((current) => current.filter((entry) => entry.id !== id)); + notificationStore.success(getAlertWebhookMutationSuccess('delete')); + } catch (error) { + logger.error('Failed to delete webhook:', error); + notificationStore.error( + error instanceof Error ? error.message : getAlertWebhookMutationFailure('delete'), + ); + } + }; + + return { + addWebhook, + deleteWebhook, + isLoadingWebhooks, + loadWebhooks, + testWebhook, + testingWebhook, + updateWebhook, + webhookLoadError, + webhooks, + }; +} diff --git a/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts b/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts index b83b62662..d449ba0a8 100644 --- a/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts +++ b/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts @@ -6,6 +6,7 @@ import alertsConfigurationModelSource from '@/features/alerts/alertsConfiguratio import alertOverridesStateSource from '@/features/alerts/useAlertOverridesState.ts?raw'; import alertDestinationsStateSource from '@/features/alerts/useAlertDestinationsState.ts?raw'; import alertDestinationsTabStateSource from '@/features/alerts/useAlertDestinationsTabState.ts?raw'; +import alertWebhookDestinationsStateSource from '@/features/alerts/useAlertWebhookDestinationsState.ts?raw'; import alertAcknowledgementStateSource from '@/features/alerts/useAlertAcknowledgementState.ts?raw'; import alertHistoryStateSource from '@/features/alerts/useAlertHistoryState.ts?raw'; import alertIncidentTimelineStateSource from '@/features/alerts/useAlertIncidentTimelineState.ts?raw'; @@ -231,9 +232,16 @@ describe('tab path helpers', () => { expect(alertDestinationsStateSource).toContain('NotificationsAPI.getEmailConfig'); expect(alertDestinationsStateSource).toContain('NotificationsAPI.updateEmailConfig'); expect(alertDestinationsTabStateSource).toContain('export function useAlertDestinationsTabState'); - expect(alertDestinationsTabStateSource).toContain('NotificationsAPI.getWebhooks'); expect(alertDestinationsTabStateSource).toContain('NotificationsAPI.testNotification'); - expect(alertDestinationsTabStateSource).toContain('NotificationsAPI.createWebhook'); + expect(alertDestinationsTabStateSource).toContain('useAlertWebhookDestinationsState'); + expect(alertDestinationsTabStateSource).not.toContain('NotificationsAPI.getWebhooks'); + expect(alertDestinationsTabStateSource).not.toContain('NotificationsAPI.createWebhook'); + expect(alertWebhookDestinationsStateSource).toContain( + 'export function useAlertWebhookDestinationsState', + ); + expect(alertWebhookDestinationsStateSource).toContain('NotificationsAPI.getWebhooks'); + expect(alertWebhookDestinationsStateSource).toContain('NotificationsAPI.createWebhook'); + expect(alertWebhookDestinationsStateSource).toContain('NotificationsAPI.testNotification'); 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 f5b94cb68..5242d89e4 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -330,6 +330,7 @@ import alertsConfigurationModelSource from '@/features/alerts/alertsConfiguratio import alertOverridesStateSource from '@/features/alerts/useAlertOverridesState.ts?raw'; import alertDestinationsStateSource from '@/features/alerts/useAlertDestinationsState.ts?raw'; import alertDestinationsTabStateSource from '@/features/alerts/useAlertDestinationsTabState.ts?raw'; +import alertWebhookDestinationsStateSource from '@/features/alerts/useAlertWebhookDestinationsState.ts?raw'; import alertAcknowledgementStateSource from '@/features/alerts/useAlertAcknowledgementState.ts?raw'; import alertHistoryStateSource from '@/features/alerts/useAlertHistoryState.ts?raw'; import alertIncidentTimelineStateSource from '@/features/alerts/useAlertIncidentTimelineState.ts?raw'; @@ -2810,10 +2811,18 @@ describe('frontend resource type boundaries', () => { expect(alertDestinationsStateSource).toContain('NotificationsAPI.getEmailConfig'); expect(alertDestinationsStateSource).toContain('NotificationsAPI.updateEmailConfig'); expect(alertDestinationsTabStateSource).toContain('export function useAlertDestinationsTabState'); - expect(alertDestinationsTabStateSource).toContain('NotificationsAPI.getWebhooks'); expect(alertDestinationsTabStateSource).toContain('NotificationsAPI.testNotification'); - expect(alertDestinationsTabStateSource).toContain('NotificationsAPI.createWebhook'); - expect(alertDestinationsTabStateSource).toContain('getAlertDestinationsWebhookLoadError'); + expect(alertDestinationsTabStateSource).toContain('useAlertWebhookDestinationsState'); + expect(alertDestinationsTabStateSource).not.toContain('NotificationsAPI.getWebhooks'); + expect(alertDestinationsTabStateSource).not.toContain('NotificationsAPI.createWebhook'); + expect(alertWebhookDestinationsStateSource).toContain( + 'export function useAlertWebhookDestinationsState', + ); + expect(alertWebhookDestinationsStateSource).toContain('NotificationsAPI.getWebhooks'); + expect(alertWebhookDestinationsStateSource).toContain('NotificationsAPI.createWebhook'); + expect(alertWebhookDestinationsStateSource).toContain( + 'getAlertDestinationsWebhookLoadError', + ); expect(alertDestinationsTabSource).toContain('getAlertDestinationsLoadErrorBanner'); expect(alertDestinationsTabSource).toContain('getAlertDestinationsAppriseTargetsHelp'); expect(alertDestinationsTabSource).toContain('getAlertDestinationsAppriseTestLabel');