diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index d70d9483a..89e48189e 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -177,6 +177,12 @@ Active alert card state, acknowledged badge, and primary/secondary action button presentation now route through `frontend-modern/src/utils/alertOverviewPresentation.ts` instead of remaining inline in `frontend-modern/src/features/alerts/OverviewTab.tsx`. +The canonical overview runtime owner is now +`frontend-modern/src/features/alerts/useAlertOverviewState.ts`, which owns the +derived alert read-model, Last 24 Hours stat refresh, and single/bulk +acknowledge control flow for `frontend-modern/src/features/alerts/OverviewTab.tsx`. +Future overview action or stat behavior should extend that hook instead of +putting acknowledge mutations and timer state back into the tab shell. Dashboard recent-alert rendering and dashboard alert summary/tone copy now route through that same alert overview presentation owner and the alert-owned `frontend-modern/src/components/Alerts/RecentAlertsPanel.tsx` surface instead diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 70161fca3..793c073c3 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -429,6 +429,12 @@ while `frontend-modern/src/features/alerts/OverviewTab.tsx` and surface composition. Future incident timeline fetch, note-save, or expansion control flow should extend that feature hook rather than forking back into either tab surface. +Overview alert runtime now follows that same shell-versus-runtime split. The +shell stays in `frontend-modern/src/features/alerts/OverviewTab.tsx`, while +`frontend-modern/src/features/alerts/useAlertOverviewState.ts` owns derived +alert stats, filtered ordering, and single/bulk acknowledge runtime behavior. +Future overview control flow should extend that hook rather than restoring +action timers or acknowledge mutations to the tab shell. Alert history runtime now follows that same pattern. The shell stays in `frontend-modern/src/features/alerts/tabs/HistoryTab.tsx`, while `frontend-modern/src/features/alerts/useAlertHistoryState.ts` owns history diff --git a/frontend-modern/src/features/alerts/OverviewTab.tsx b/frontend-modern/src/features/alerts/OverviewTab.tsx index fc619df8e..dc45bd92b 100644 --- a/frontend-modern/src/features/alerts/OverviewTab.tsx +++ b/frontend-modern/src/features/alerts/OverviewTab.tsx @@ -1,12 +1,13 @@ import { InvestigateAlertButton } from '@/components/Alerts/InvestigateAlertButton'; -import { createSignal, createMemo, onCleanup, For, Show, createEffect } from 'solid-js'; +import { createSignal, onCleanup, For, Show, createEffect } from 'solid-js'; import { useLocation } from '@solidjs/router'; import type { Alert } from '@/types/api'; import type { Override } from './types'; import { alertTypeDisplayLabel } from './helpers'; import { getCanonicalAlertId } from './identity'; +import { useAlertOverviewState } from './useAlertOverviewState'; import { useAlertIncidentTimelineState } from './useAlertIncidentTimelineState'; import { Card } from '@/components/shared/Card'; import { SectionHeader } from '@/components/shared/SectionHeader'; @@ -21,8 +22,7 @@ import { getAlertOverviewSecondaryActionClass, getAlertOverviewStartedAtClass, } from '@/utils/alertOverviewPresentation'; - -// Overview Tab - Shows current alert status + export function OverviewTab(props: { overrides: Override[]; activeAlerts: Record; @@ -37,9 +37,20 @@ export function OverviewTab(props: { }) { const location = useLocation(); let hashScrollRafId: number | undefined; - const pendingProcessingResetTimeouts = new Set(); - // Loading states for buttons - const [processingAlerts, setProcessingAlerts] = createSignal>(new Set()); + const [lastHashScrolled, setLastHashScrolled] = createSignal(null); + const { + alertStats, + filteredAlerts, + processingAlerts, + bulkAckProcessing, + handleAlertAcknowledgement, + handleBulkAcknowledge, + } = useAlertOverviewState({ + activeAlerts: () => props.activeAlerts, + overrides: () => props.overrides, + showAcknowledged: props.showAcknowledged, + updateAlert: props.updateAlert, + }); const { incidentTimelines, incidentLoading, @@ -54,63 +65,6 @@ export function OverviewTab(props: { setIncidentNoteDraft, saveIncidentNote, } = useAlertIncidentTimelineState(); - const [lastHashScrolled, setLastHashScrolled] = createSignal(null); - // Tick every 60s so the "Last 24 Hours" count stays fresh as alerts age out - const [tick, setTick] = createSignal(Date.now()); - const tickInterval = setInterval(() => setTick(Date.now()), 60_000); - onCleanup(() => clearInterval(tickInterval)); - const processingReleaseTimers = new Map>(); - - const clearProcessingReleaseTimer = (alertIdentifier: string) => { - const timer = processingReleaseTimers.get(alertIdentifier); - if (timer === undefined) { - return; - } - clearTimeout(timer); - processingReleaseTimers.delete(alertIdentifier); - }; - - onCleanup(() => { - processingReleaseTimers.forEach((timer) => clearTimeout(timer)); - processingReleaseTimers.clear(); - }); - - // Get alert stats from actual active alerts - const alertStats = createMemo(() => { - // Access the store properly for reactivity - const alertIds = Object.keys(props.activeAlerts); - const alerts = alertIds.map((id) => props.activeAlerts[id]); - return { - active: alerts.filter((a) => !a.acknowledged).length, - acknowledged: alerts.filter((a) => a.acknowledged).length, - total24h: alerts.filter((a) => { - const age = tick() - new Date(a.startTime).getTime(); - return age >= 0 && age < 86_400_000; - }).length, - overrides: props.overrides.length, - }; - }); - - const filteredAlerts = createMemo(() => { - const alerts = Object.values(props.activeAlerts); - // Sort: unacknowledged first, then by start time (newest first) - return alerts - .filter((alert) => props.showAcknowledged() || !alert.acknowledged) - .sort((a, b) => { - // Acknowledged status comparison first - if (a.acknowledged !== b.acknowledged) { - return a.acknowledged ? 1 : -1; // Unacknowledged first - } - // Then by time - return new Date(b.startTime).getTime() - new Date(a.startTime).getTime(); - }); - }); - - const unacknowledgedAlerts = createMemo(() => - Object.values(props.activeAlerts).filter((alert) => !alert.acknowledged), - ); - - const [bulkAckProcessing, setBulkAckProcessing] = createSignal(false); const scrollToAlertHash = () => { const hash = location.hash; @@ -147,10 +101,6 @@ export function OverviewTab(props: { cancelAnimationFrame(hashScrollRafId); hashScrollRafId = undefined; } - pendingProcessingResetTimeouts.forEach((timeoutId) => { - window.clearTimeout(timeoutId); - }); - pendingProcessingResetTimeouts.clear(); }); return ( @@ -319,44 +269,8 @@ export function OverviewTab(props: { type="button" class="inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium rounded-md border border-blue-200 dark:border-blue-700 bg-blue-50 dark:bg-blue-900 text-blue-700 dark:text-blue-200 transition-colors hover:bg-blue-100 dark:hover:bg-blue-900 disabled:opacity-60 disabled:cursor-not-allowed" disabled={bulkAckProcessing()} - onClick={async () => { - if (bulkAckProcessing()) return; - const pending = unacknowledgedAlerts(); - if (pending.length === 0) { - return; - } - setBulkAckProcessing(true); - try { - const result = await AlertsAPI.bulkAcknowledge( - pending.map((alert) => getCanonicalAlertId(alert)), - ); - const successes = result.results.filter((r) => r.success); - const failures = result.results.filter((r) => !r.success); - - successes.forEach((res) => { - props.updateAlert(res.alertIdentifier, { - acknowledged: true, - ackTime: new Date().toISOString(), - }); - }); - - if (successes.length > 0) { - notificationStore.success( - `Acknowledged ${successes.length} ${successes.length === 1 ? 'alert' : 'alerts'}.`, - ); - } - - if (failures.length > 0) { - notificationStore.error( - `Failed to acknowledge ${failures.length} ${failures.length === 1 ? 'alert' : 'alerts'}.`, - ); - } - } catch (error) { - logger.error('Bulk acknowledge failed', error); - notificationStore.error('Failed to acknowledge alerts'); - } finally { - setBulkAckProcessing(false); - } + onClick={() => { + void handleBulkAcknowledge(); }} > {bulkAckProcessing() @@ -454,62 +368,7 @@ export function OverviewTab(props: { onClick={async (e) => { e.preventDefault(); e.stopPropagation(); - - const alertIdentifier = getCanonicalAlertId(alert); - - // Prevent double-clicks - if (processingAlerts().has(alertIdentifier)) return; - - setProcessingAlerts( - (prev) => new Set(prev).add(alertIdentifier), - ); - - // Store current state to avoid race conditions - const wasAcknowledged = alert.acknowledged; - - try { - if (wasAcknowledged) { - // Call API first, only update local state if successful - await AlertsAPI.unacknowledge(alertIdentifier); - // Only update local state after successful API call - props.updateAlert(alertIdentifier, { - acknowledged: false, - ackTime: undefined, - ackUser: undefined, - }); - notificationStore.success('Alert restored'); - } else { - // Call API first, only update local state if successful - await AlertsAPI.acknowledge(alertIdentifier); - // Only update local state after successful API call - props.updateAlert(alertIdentifier, { - acknowledged: true, - ackTime: new Date().toISOString(), - }); - notificationStore.success('Alert acknowledged'); - } - } catch (err) { - logger.error( - `Failed to ${wasAcknowledged ? 'unacknowledge' : 'acknowledge'} alert:`, - err, - ); - notificationStore.error( - `Failed to ${wasAcknowledged ? 'restore' : 'acknowledge'} alert`, - ); - // Don't update local state on error - let WebSocket keep the correct state - } finally { - // Keep button disabled for longer to prevent race conditions with WebSocket updates - clearProcessingReleaseTimer(alertIdentifier); - const timer = setTimeout(() => { - processingReleaseTimers.delete(alertIdentifier); - setProcessingAlerts((prev) => { - const next = new Set(prev); - next.delete(alertIdentifier); - return next; - }); - }, 1500); // 1.5 seconds to allow server to process and WebSocket to sync - processingReleaseTimers.set(alertIdentifier, timer); - } + await handleAlertAcknowledgement(alert); }} > {processingAlerts().has(getCanonicalAlertId(alert)) diff --git a/frontend-modern/src/features/alerts/__tests__/useAlertOverviewState.test.tsx b/frontend-modern/src/features/alerts/__tests__/useAlertOverviewState.test.tsx new file mode 100644 index 000000000..75fc27d40 --- /dev/null +++ b/frontend-modern/src/features/alerts/__tests__/useAlertOverviewState.test.tsx @@ -0,0 +1,119 @@ +import { renderHook } from '@solidjs/testing-library'; +import { createSignal } from 'solid-js'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { AlertsAPI } from '@/api/alerts'; +import { notificationStore } from '@/stores/notifications'; +import type { Alert } from '@/types/api'; + +import { useAlertOverviewState } from '../useAlertOverviewState'; + +vi.mock('@/api/alerts', () => ({ + AlertsAPI: { + acknowledge: vi.fn(), + bulkAcknowledge: vi.fn(), + unacknowledge: vi.fn(), + }, +})); + +vi.mock('@/stores/notifications', () => ({ + notificationStore: { + error: vi.fn(), + success: vi.fn(), + }, +})); + +vi.mock('@/utils/logger', () => ({ + logger: { + error: vi.fn(), + }, +})); + +function makeAlert(id: string, startTime: string, acknowledged = false): Alert { + return { + id, + type: 'cpu', + level: 'warning', + resourceId: `vm-${id}`, + resourceName: `VM ${id}`, + node: 'node-1', + message: `CPU high on ${id}`, + startTime, + acknowledged, + } as Alert; +} + +describe('useAlertOverviewState', () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-03-22T12:00:00Z')); + vi.mocked(AlertsAPI.acknowledge).mockReset(); + vi.mocked(AlertsAPI.unacknowledge).mockReset(); + vi.mocked(AlertsAPI.bulkAcknowledge).mockReset(); + vi.mocked(notificationStore.success).mockReset(); + vi.mocked(notificationStore.error).mockReset(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('owns overview stats, filtering, and acknowledge flows outside the tab shell', async () => { + const now = Date.now(); + const [activeAlerts] = createSignal>({ + warning: makeAlert('warning', new Date(now - 60_000).toISOString(), false), + acknowledged: makeAlert('acknowledged', new Date(now - 2 * 60_000).toISOString(), true), + old: makeAlert('old', new Date(now - 3 * 86_400_000).toISOString(), false), + }); + const [showAcknowledged] = createSignal(false); + const updateAlert = vi.fn(); + + vi.mocked(AlertsAPI.acknowledge).mockResolvedValue(undefined as any); + vi.mocked(AlertsAPI.unacknowledge).mockResolvedValue(undefined as any); + vi.mocked(AlertsAPI.bulkAcknowledge).mockResolvedValue({ + results: [ + { alertIdentifier: 'warning', success: true }, + { alertIdentifier: 'old', success: false }, + ], + } as any); + + const { result } = renderHook(() => + useAlertOverviewState({ + activeAlerts, + overrides: () => [], + showAcknowledged, + updateAlert, + }), + ); + + expect(result.alertStats()).toMatchObject({ + active: 2, + acknowledged: 1, + total24h: 2, + overrides: 0, + }); + expect(result.filteredAlerts().map((alert) => alert.id)).toEqual(['warning', 'old']); + + await result.handleAlertAcknowledgement(activeAlerts().warning); + + expect(AlertsAPI.acknowledge).toHaveBeenCalledWith('warning'); + expect(updateAlert).toHaveBeenCalledWith( + 'warning', + expect.objectContaining({ acknowledged: true }), + ); + expect(notificationStore.success).toHaveBeenCalledWith('Alert acknowledged'); + expect(result.processingAlerts().has('warning')).toBe(true); + + vi.advanceTimersByTime(1500); + expect(result.processingAlerts().has('warning')).toBe(false); + + await result.handleBulkAcknowledge(); + + expect(AlertsAPI.bulkAcknowledge).toHaveBeenCalledWith(['warning', 'old']); + expect(updateAlert).toHaveBeenCalledWith( + 'warning', + expect.objectContaining({ acknowledged: true }), + ); + expect(notificationStore.error).toHaveBeenCalledWith('Failed to acknowledge 1 alert.'); + }); +}); diff --git a/frontend-modern/src/features/alerts/useAlertOverviewState.ts b/frontend-modern/src/features/alerts/useAlertOverviewState.ts new file mode 100644 index 000000000..f67d4b089 --- /dev/null +++ b/frontend-modern/src/features/alerts/useAlertOverviewState.ts @@ -0,0 +1,172 @@ +import { createMemo, createSignal, onCleanup } from 'solid-js'; +import type { Accessor } from 'solid-js'; + +import { AlertsAPI } from '@/api/alerts'; +import { notificationStore } from '@/stores/notifications'; +import type { Alert } from '@/types/api'; +import { logger } from '@/utils/logger'; + +import { getCanonicalAlertId } from './identity'; +import type { Override } from './types'; + +export interface UseAlertOverviewStateProps { + activeAlerts: Accessor>; + overrides: Accessor; + showAcknowledged: Accessor; + updateAlert: (alertIdentifier: string, updates: Partial) => void; +} + +export function useAlertOverviewState(props: UseAlertOverviewStateProps) { + const [processingAlerts, setProcessingAlerts] = createSignal>(new Set()); + const [bulkAckProcessing, setBulkAckProcessing] = createSignal(false); + const [tick, setTick] = createSignal(Date.now()); + const tickInterval = setInterval(() => setTick(Date.now()), 60_000); + const processingReleaseTimers = new Map>(); + + const clearProcessingReleaseTimer = (alertIdentifier: string) => { + const timer = processingReleaseTimers.get(alertIdentifier); + if (timer === undefined) { + return; + } + clearTimeout(timer); + processingReleaseTimers.delete(alertIdentifier); + }; + + onCleanup(() => { + clearInterval(tickInterval); + processingReleaseTimers.forEach((timer) => clearTimeout(timer)); + processingReleaseTimers.clear(); + }); + + const alertStats = createMemo(() => { + const alerts = Object.values(props.activeAlerts()); + return { + active: alerts.filter((alert) => !alert.acknowledged).length, + acknowledged: alerts.filter((alert) => alert.acknowledged).length, + total24h: alerts.filter((alert) => { + const age = tick() - new Date(alert.startTime).getTime(); + return age >= 0 && age < 86_400_000; + }).length, + overrides: props.overrides().length, + }; + }); + + const filteredAlerts = createMemo(() => + Object.values(props.activeAlerts()) + .filter((alert) => props.showAcknowledged() || !alert.acknowledged) + .sort((a, b) => { + if (a.acknowledged !== b.acknowledged) { + return a.acknowledged ? 1 : -1; + } + return new Date(b.startTime).getTime() - new Date(a.startTime).getTime(); + }), + ); + + const unacknowledgedAlerts = createMemo(() => + Object.values(props.activeAlerts()).filter((alert) => !alert.acknowledged), + ); + + const releaseAlertProcessing = (alertIdentifier: string) => { + clearProcessingReleaseTimer(alertIdentifier); + const timer = setTimeout(() => { + processingReleaseTimers.delete(alertIdentifier); + setProcessingAlerts((prev) => { + const next = new Set(prev); + next.delete(alertIdentifier); + return next; + }); + }, 1500); + processingReleaseTimers.set(alertIdentifier, timer); + }; + + const handleAlertAcknowledgement = async (alert: Alert) => { + const alertIdentifier = getCanonicalAlertId(alert); + if (processingAlerts().has(alertIdentifier)) { + return; + } + + setProcessingAlerts((prev) => new Set(prev).add(alertIdentifier)); + const wasAcknowledged = alert.acknowledged; + + try { + if (wasAcknowledged) { + await AlertsAPI.unacknowledge(alertIdentifier); + props.updateAlert(alertIdentifier, { + acknowledged: false, + ackTime: undefined, + ackUser: undefined, + }); + notificationStore.success('Alert restored'); + } else { + await AlertsAPI.acknowledge(alertIdentifier); + props.updateAlert(alertIdentifier, { + acknowledged: true, + ackTime: new Date().toISOString(), + }); + notificationStore.success('Alert acknowledged'); + } + } catch (error) { + logger.error( + `Failed to ${wasAcknowledged ? 'unacknowledge' : 'acknowledge'} alert:`, + error, + ); + notificationStore.error(`Failed to ${wasAcknowledged ? 'restore' : 'acknowledge'} alert`); + } finally { + releaseAlertProcessing(alertIdentifier); + } + }; + + const handleBulkAcknowledge = async () => { + if (bulkAckProcessing()) { + return; + } + + const pending = unacknowledgedAlerts(); + if (pending.length === 0) { + return; + } + + setBulkAckProcessing(true); + try { + const result = await AlertsAPI.bulkAcknowledge( + pending.map((alert) => getCanonicalAlertId(alert)), + ); + const successes = result.results.filter((entry) => entry.success); + const failures = result.results.filter((entry) => !entry.success); + + successes.forEach((entry) => { + props.updateAlert(entry.alertIdentifier, { + acknowledged: true, + ackTime: new Date().toISOString(), + }); + }); + + if (successes.length > 0) { + notificationStore.success( + `Acknowledged ${successes.length} ${successes.length === 1 ? 'alert' : 'alerts'}.`, + ); + } + + if (failures.length > 0) { + notificationStore.error( + `Failed to acknowledge ${failures.length} ${failures.length === 1 ? 'alert' : 'alerts'}.`, + ); + } + } catch (error) { + logger.error('Bulk acknowledge failed', error); + notificationStore.error('Failed to acknowledge alerts'); + } finally { + setBulkAckProcessing(false); + } + }; + + return { + alertStats, + filteredAlerts, + unacknowledgedAlerts, + processingAlerts, + bulkAckProcessing, + handleAlertAcknowledgement, + handleBulkAcknowledge, + }; +} diff --git a/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts b/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts index ea1390526..817833eaa 100644 --- a/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts +++ b/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts @@ -5,8 +5,10 @@ import alertsConfigurationStateSource from '@/features/alerts/useAlertsConfigura import alertDestinationsStateSource from '@/features/alerts/useAlertDestinationsState.ts?raw'; import alertHistoryStateSource from '@/features/alerts/useAlertHistoryState.ts?raw'; import alertIncidentTimelineStateSource from '@/features/alerts/useAlertIncidentTimelineState.ts?raw'; +import alertOverviewStateSource from '@/features/alerts/useAlertOverviewState.ts?raw'; import alertDestinationsTabSource from '@/features/alerts/tabs/DestinationsTab.tsx?raw'; import alertHistoryTabSource from '@/features/alerts/tabs/HistoryTab.tsx?raw'; +import alertOverviewTabSource from '@/features/alerts/OverviewTab.tsx?raw'; import alertScheduleTabSource from '@/features/alerts/tabs/ScheduleTab.tsx?raw'; import alertThresholdsTabSource from '@/features/alerts/tabs/ThresholdsTab.tsx?raw'; import thresholdsTableSource from '@/components/Alerts/ThresholdsTable.tsx?raw'; @@ -221,6 +223,14 @@ describe('tab path helpers', () => { ); expect(alertIncidentTimelineStateSource).toContain('AlertsAPI.getIncidentTimeline'); expect(alertIncidentTimelineStateSource).toContain('AlertsAPI.addIncidentNote'); + expect(alertOverviewTabSource).toContain('useAlertOverviewState'); + expect(alertOverviewTabSource).not.toContain('AlertsAPI.bulkAcknowledge'); + expect(alertOverviewTabSource).not.toContain('AlertsAPI.acknowledge'); + expect(alertOverviewTabSource).not.toContain('AlertsAPI.unacknowledge'); + expect(alertOverviewStateSource).toContain('export function useAlertOverviewState'); + expect(alertOverviewStateSource).toContain('AlertsAPI.bulkAcknowledge'); + expect(alertOverviewStateSource).toContain('AlertsAPI.acknowledge'); + expect(alertOverviewStateSource).toContain('AlertsAPI.unacknowledge'); expect(alertScheduleTabSource).toContain('getAlertConfigQuietHourSuppressOptions'); expect(alertThresholdsTabSource).toContain('ThresholdsTable'); expect(thresholdsTableSource).toContain( diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index 2aee3b6af..e1ad64e9a 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -329,6 +329,7 @@ import alertsConfigurationStateSource from '@/features/alerts/useAlertsConfigura import alertDestinationsStateSource from '@/features/alerts/useAlertDestinationsState.ts?raw'; import alertHistoryStateSource from '@/features/alerts/useAlertHistoryState.ts?raw'; import alertIncidentTimelineStateSource from '@/features/alerts/useAlertIncidentTimelineState.ts?raw'; +import alertOverviewStateSource from '@/features/alerts/useAlertOverviewState.ts?raw'; import alertDestinationsTabSource from '@/features/alerts/tabs/DestinationsTab.tsx?raw'; import alertHistoryTabSource from '@/features/alerts/tabs/HistoryTab.tsx?raw'; import alertScheduleTabSource from '@/features/alerts/tabs/ScheduleTab.tsx?raw'; @@ -3612,6 +3613,7 @@ describe('frontend resource type boundaries', () => { it('keeps alert incident timeline state copy in a shared presentation utility', () => { expect(alertOverviewTabSource).toContain('IncidentTimelinePanel'); expect(alertOverviewTabSource).toContain('useAlertIncidentTimelineState'); + expect(alertOverviewTabSource).toContain('useAlertOverviewState'); expect(alertIncidentEventFiltersSource).toContain('getAlertIncidentEventFilterContainerClass'); expect(alertIncidentEventFiltersSource).toContain('getAlertIncidentEventFilterChipClass'); expect(alertIncidentEventFiltersSource).toContain( @@ -3644,6 +3646,9 @@ describe('frontend resource type boundaries', () => { expect(alertOverviewTabSource).toContain('getAlertOverviewStartedAtClass'); expect(alertOverviewTabSource).toContain('getAlertOverviewPrimaryActionClass'); expect(alertOverviewTabSource).toContain('getAlertOverviewSecondaryActionClass'); + expect(alertOverviewTabSource).not.toContain('AlertsAPI.bulkAcknowledge'); + expect(alertOverviewTabSource).not.toContain('AlertsAPI.acknowledge'); + expect(alertOverviewTabSource).not.toContain('AlertsAPI.unacknowledge'); expect(alertOverviewTabSource).not.toContain('AlertsAPI.getIncidentTimeline'); expect(alertOverviewTabSource).not.toContain('AlertsAPI.addIncidentNote'); expect(alertOverviewTabSource).not.toContain('Loading timeline...'); @@ -3651,6 +3656,10 @@ describe('frontend resource type boundaries', () => { expect(alertOverviewTabSource).not.toContain('No timeline events yet.'); expect(alertOverviewTabSource).not.toContain('No incident timeline available.'); expect(alertOverviewTabSource).not.toContain('Failed to load timeline.'); + expect(alertOverviewStateSource).toContain('export function useAlertOverviewState'); + expect(alertOverviewStateSource).toContain('AlertsAPI.bulkAcknowledge'); + expect(alertOverviewStateSource).toContain('AlertsAPI.acknowledge'); + expect(alertOverviewStateSource).toContain('AlertsAPI.unacknowledge'); expect(alertOverviewPresentationSource).toContain( 'export function getAlertTimelineLoadingState', );