diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index 105cb24554..d30296dccc 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -154,6 +154,13 @@ Expanded alert incident detail now also routes through overview surface and the history table share the same loading/error states, canonical timeline meta row, note editor, and event-filter controls instead of maintaining two independent incident-detail implementations. +That shared timeline runtime state now routes through +`frontend-modern/src/features/alerts/useAlertIncidentTimelineState.ts`, which +owns incident timeline fetch, expansion state, note-save flow, and shared +event-filter state for both `frontend-modern/src/features/alerts/OverviewTab.tsx` +and `frontend-modern/src/features/alerts/tabs/HistoryTab.tsx`. Future incident +timeline control flow should land in that feature hook instead of being +forked back into either alert surface. Resource incident panel cards, summary rows, and toggle-button presentation now also route through `frontend-modern/src/utils/alertIncidentPresentation.ts` diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 104b7f3e44..b8c8a8d8c8 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -423,6 +423,13 @@ 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. +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 +`frontend-modern/src/features/alerts/tabs/HistoryTab.tsx` stay focused on +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. Top-level settings surfaces must route through `Settings.tsx`, `SettingsPageShell.tsx`, and `frontend-modern/src/components/shared/SettingsPanel.tsx` instead of diff --git a/frontend-modern/src/features/alerts/OverviewTab.tsx b/frontend-modern/src/features/alerts/OverviewTab.tsx index c47a5296df..fc619df8ee 100644 --- a/frontend-modern/src/features/alerts/OverviewTab.tsx +++ b/frontend-modern/src/features/alerts/OverviewTab.tsx @@ -2,18 +2,15 @@ import { InvestigateAlertButton } from '@/components/Alerts/InvestigateAlertButt import { createSignal, createMemo, onCleanup, For, Show, createEffect } from 'solid-js'; import { useLocation } from '@solidjs/router'; -import type { Incident } from '@/types/api'; -import { AlertsAPI } from '@/api/alerts'; import type { Alert } from '@/types/api'; import type { Override } from './types'; import { alertTypeDisplayLabel } from './helpers'; import { getCanonicalAlertId } from './identity'; +import { useAlertIncidentTimelineState } from './useAlertIncidentTimelineState'; import { Card } from '@/components/shared/Card'; import { SectionHeader } from '@/components/shared/SectionHeader'; import { IncidentTimelinePanel } from '@/components/Alerts/IncidentTimelinePanel'; -import { notificationStore } from '@/stores/notifications'; -import { logger } from '@/utils/logger'; import { ALERTS_EMPTY_STATE, ALERTS_THRESHOLD_HINT, @@ -24,7 +21,6 @@ import { getAlertOverviewSecondaryActionClass, getAlertOverviewStartedAtClass, } from '@/utils/alertOverviewPresentation'; -import { INCIDENT_EVENT_TYPES } from './types'; // Overview Tab - Shows current alert status export function OverviewTab(props: { @@ -44,17 +40,20 @@ export function OverviewTab(props: { const pendingProcessingResetTimeouts = new Set(); // Loading states for buttons const [processingAlerts, setProcessingAlerts] = createSignal>(new Set()); - const [incidentTimelines, setIncidentTimelines] = createSignal>( - {}, - ); - const [incidentLoading, setIncidentLoading] = createSignal>({}); - const [incidentErrors, setIncidentErrors] = createSignal>({}); - const [expandedIncidents, setExpandedIncidents] = createSignal>(new Set()); - const [incidentNoteDrafts, setIncidentNoteDrafts] = createSignal>({}); - const [incidentNoteSaving, setIncidentNoteSaving] = createSignal>(new Set()); - const [incidentEventFilters, setIncidentEventFilters] = createSignal>( - new Set(INCIDENT_EVENT_TYPES), - ); + const { + incidentTimelines, + incidentLoading, + incidentErrors, + expandedIncidents, + incidentNoteDrafts, + incidentNoteSaving, + eventFilters: incidentEventFilters, + setEventFilters: setIncidentEventFilters, + loadIncidentTimeline, + toggleIncidentTimeline, + 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()); @@ -76,60 +75,6 @@ export function OverviewTab(props: { processingReleaseTimers.clear(); }); - const loadIncidentTimeline = async (alertIdentifier: string, startedAt?: string) => { - setIncidentLoading((prev) => ({ ...prev, [alertIdentifier]: true })); - try { - const timeline = await AlertsAPI.getIncidentTimeline(alertIdentifier, startedAt); - setIncidentTimelines((prev) => ({ ...prev, [alertIdentifier]: timeline })); - setIncidentErrors((prev) => ({ ...prev, [alertIdentifier]: false })); - } catch (error) { - logger.error('Failed to load incident timeline', error); - notificationStore.error('Failed to load incident timeline'); - setIncidentErrors((prev) => ({ ...prev, [alertIdentifier]: true })); - } finally { - setIncidentLoading((prev) => ({ ...prev, [alertIdentifier]: false })); - } - }; - - const toggleIncidentTimeline = async (alertIdentifier: string, startedAt?: string) => { - const expanded = expandedIncidents(); - const next = new Set(expanded); - if (next.has(alertIdentifier)) { - next.delete(alertIdentifier); - setExpandedIncidents(next); - return; - } - next.add(alertIdentifier); - setExpandedIncidents(next); - if (!(alertIdentifier in incidentTimelines())) { - await loadIncidentTimeline(alertIdentifier, startedAt); - } - }; - - const saveIncidentNote = async (alertIdentifier: string, startedAt?: string) => { - const note = (incidentNoteDrafts()[alertIdentifier] || '').trim(); - if (!note) { - return; - } - setIncidentNoteSaving((prev) => new Set(prev).add(alertIdentifier)); - try { - const incidentId = incidentTimelines()[alertIdentifier]?.id; - await AlertsAPI.addIncidentNote({ alertIdentifier, incidentId, note }); - setIncidentNoteDrafts((prev) => ({ ...prev, [alertIdentifier]: '' })); - await loadIncidentTimeline(alertIdentifier, startedAt); - notificationStore.success('Incident note saved'); - } catch (error) { - logger.error('Failed to save incident note', error); - notificationStore.error('Failed to save incident note'); - } finally { - setIncidentNoteSaving((prev) => { - const next = new Set(prev); - next.delete(alertIdentifier); - return next; - }); - } - }; - // Get alert stats from actual active alerts const alertStats = createMemo(() => { // Access the store properly for reactivity @@ -608,10 +553,7 @@ export function OverviewTab(props: { eventCardVariant="alt" noteDraft={incidentNoteDrafts()[getCanonicalAlertId(alert)] || ''} onNoteDraftChange={(value) => - setIncidentNoteDrafts((prev) => ({ - ...prev, - [getCanonicalAlertId(alert)]: value, - })) + setIncidentNoteDraft(getCanonicalAlertId(alert), value) } noteSaving={incidentNoteSaving().has(getCanonicalAlertId(alert))} onSaveNote={() => { diff --git a/frontend-modern/src/features/alerts/__tests__/useAlertIncidentTimelineState.test.tsx b/frontend-modern/src/features/alerts/__tests__/useAlertIncidentTimelineState.test.tsx new file mode 100644 index 0000000000..8bcf1fd854 --- /dev/null +++ b/frontend-modern/src/features/alerts/__tests__/useAlertIncidentTimelineState.test.tsx @@ -0,0 +1,76 @@ +import { renderHook } from '@solidjs/testing-library'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { AlertsAPI } from '@/api/alerts'; +import { notificationStore } from '@/stores/notifications'; + +import { INCIDENT_EVENT_TYPES } from '../types'; +import { useAlertIncidentTimelineState } from '../useAlertIncidentTimelineState'; + +vi.mock('@/api/alerts', () => ({ + AlertsAPI: { + addIncidentNote: vi.fn(), + getIncidentTimeline: vi.fn(), + }, +})); + +vi.mock('@/stores/notifications', () => ({ + notificationStore: { + error: vi.fn(), + success: vi.fn(), + }, +})); + +vi.mock('@/utils/logger', () => ({ + logger: { + error: vi.fn(), + }, +})); + +describe('useAlertIncidentTimelineState', () => { + beforeEach(() => { + vi.mocked(AlertsAPI.getIncidentTimeline).mockReset(); + vi.mocked(AlertsAPI.addIncidentNote).mockReset(); + vi.mocked(notificationStore.success).mockReset(); + vi.mocked(notificationStore.error).mockReset(); + }); + + it('owns shared incident timeline load, note-save, and reset behavior for alert surfaces', async () => { + vi.mocked(AlertsAPI.getIncidentTimeline).mockResolvedValue({ + id: 'incident-1', + events: [], + } as any); + vi.mocked(AlertsAPI.addIncidentNote).mockResolvedValue(undefined as any); + + const { result } = renderHook(() => useAlertIncidentTimelineState()); + + expect(Array.from(result.eventFilters())).toEqual(INCIDENT_EVENT_TYPES); + + await result.toggleIncidentTimeline('row-1', 'alert-1', '2026-03-01T00:00:00Z'); + + expect(result.expandedIncidents().has('row-1')).toBe(true); + expect(AlertsAPI.getIncidentTimeline).toHaveBeenCalledWith( + 'alert-1', + '2026-03-01T00:00:00Z', + ); + expect(result.incidentTimelines()['row-1']).toMatchObject({ id: 'incident-1' }); + + result.setIncidentNoteDraft('row-1', 'operator note'); + await result.saveIncidentNote('row-1', 'alert-1', '2026-03-01T00:00:00Z'); + + expect(AlertsAPI.addIncidentNote).toHaveBeenCalledWith({ + alertIdentifier: 'alert-1', + incidentId: 'incident-1', + note: 'operator note', + }); + expect(notificationStore.success).toHaveBeenCalledWith('Incident note saved'); + expect(result.incidentNoteDrafts()['row-1']).toBe(''); + + result.setEventFilters(new Set(['note'])); + result.resetState(); + + expect(result.incidentTimelines()).toEqual({}); + expect(result.expandedIncidents().size).toBe(0); + expect(Array.from(result.eventFilters())).toEqual(INCIDENT_EVENT_TYPES); + }); +}); diff --git a/frontend-modern/src/features/alerts/tabs/HistoryTab.tsx b/frontend-modern/src/features/alerts/tabs/HistoryTab.tsx index 605431394d..27e3d4bfd9 100644 --- a/frontend-modern/src/features/alerts/tabs/HistoryTab.tsx +++ b/frontend-modern/src/features/alerts/tabs/HistoryTab.tsx @@ -65,12 +65,9 @@ import { getAlertResourceIncidentFilteredEventsEmptyState, getAlertResourceIncidentLoadFailure, getAlertResourceIncidentLoadingState, - getAlertResourceIncidentNoteSaveFailure, - getAlertResourceIncidentNoteSavedLabel, getAlertResourceIncidentPanelTitle, getAlertResourceIncidentRefreshLabel, getAlertResourceIncidentSummaryRowClass, - getAlertResourceIncidentTimelineFailure, getAlertResourceIncidentToggleButtonClass, getAlertResourceIncidentToggleLabel, getAlertResourceIncidentTruncatedEventsLabel, @@ -83,6 +80,7 @@ import { alertTypeDisplayLabel, unifiedTypeToAlertDisplayType, } from '../helpers'; +import { useAlertIncidentTimelineState } from '../useAlertIncidentTimelineState'; import { filterIncidentEvents, INCIDENT_EVENT_TYPES, @@ -134,22 +132,26 @@ export function HistoryTab(props: HistoryTabProps) { const [expandedResourceIncidentIds, setExpandedResourceIncidentIds] = createSignal>( new Set(), ); - const [historyIncidentEventFilters, setHistoryIncidentEventFilters] = createSignal>( - new Set(INCIDENT_EVENT_TYPES), - ); const [resourceIncidentEventFilters, setResourceIncidentEventFilters] = createSignal>( new Set(INCIDENT_EVENT_TYPES), ); - const [incidentTimelines, setIncidentTimelines] = createSignal>( - {}, - ); - const [incidentLoading, setIncidentLoading] = createSignal>({}); - const [incidentErrors, setIncidentErrors] = createSignal>({}); - const [expandedIncidents, setExpandedIncidents] = createSignal>(new Set()); - const [incidentNoteDrafts, setIncidentNoteDrafts] = createSignal>({}); - const [incidentNoteSaving, setIncidentNoteSaving] = createSignal>(new Set()); const { isMobile } = useBreakpoint(); const [filtersOpen, setFiltersOpen] = createSignal(false); + const { + incidentTimelines, + incidentLoading, + incidentErrors, + expandedIncidents, + incidentNoteDrafts, + incidentNoteSaving, + eventFilters: historyIncidentEventFilters, + setEventFilters: setHistoryIncidentEventFilters, + resetState: resetIncidentTimelineState, + loadIncidentTimeline, + toggleIncidentTimeline, + setIncidentNoteDraft, + saveIncidentNote, + } = useAlertIncidentTimelineState(); const activeFilterCount = createMemo(() => { let count = 0; @@ -241,12 +243,7 @@ export function HistoryTab(props: HistoryTabProps) { setResourceIncidents({}); setResourceIncidentLoading({}); setExpandedResourceIncidentIds(new Set()); - setIncidentTimelines({}); - setIncidentLoading({}); - setIncidentErrors({}); - setExpandedIncidents(new Set()); - setIncidentNoteDrafts({}); - setIncidentNoteSaving(new Set()); + resetIncidentTimelineState(); void fetchHistory(timeFilter()); }); @@ -700,72 +697,6 @@ export function HistoryTab(props: HistoryTabProps) { }; }); - const loadIncidentTimeline = async ( - rowKey: string, - alertIdentifier: string, - startedAt?: string, - ) => { - setIncidentLoading((prev) => ({ ...prev, [rowKey]: true })); - try { - const timeline = await AlertsAPI.getIncidentTimeline(alertIdentifier, startedAt); - setIncidentTimelines((prev) => ({ ...prev, [rowKey]: timeline })); - setIncidentErrors((prev) => ({ ...prev, [rowKey]: false })); - } catch (error) { - logger.error(getAlertResourceIncidentTimelineFailure(), error); - notificationStore.error(getAlertResourceIncidentTimelineFailure()); - setIncidentErrors((prev) => ({ ...prev, [rowKey]: true })); - } finally { - setIncidentLoading((prev) => ({ ...prev, [rowKey]: false })); - } - }; - - const toggleIncidentTimeline = async ( - rowKey: string, - alertIdentifier: string, - startedAt?: string, - ) => { - const expanded = expandedIncidents(); - const next = new Set(expanded); - if (next.has(rowKey)) { - next.delete(rowKey); - setExpandedIncidents(next); - return; - } - next.add(rowKey); - setExpandedIncidents(next); - if (!(rowKey in incidentTimelines())) { - await loadIncidentTimeline(rowKey, alertIdentifier, startedAt); - } - }; - - const saveIncidentNote = async ( - rowKey: string, - alertIdentifier: string, - startedAt?: string, - ) => { - const note = (incidentNoteDrafts()[rowKey] || '').trim(); - if (!note) { - return; - } - setIncidentNoteSaving((prev) => new Set(prev).add(rowKey)); - try { - const incidentId = incidentTimelines()[rowKey]?.id; - await AlertsAPI.addIncidentNote({ alertIdentifier, incidentId, note }); - setIncidentNoteDrafts((prev) => ({ ...prev, [rowKey]: '' })); - await loadIncidentTimeline(rowKey, alertIdentifier, startedAt); - notificationStore.success(getAlertResourceIncidentNoteSavedLabel()); - } catch (error) { - logger.error(getAlertResourceIncidentNoteSaveFailure(), error); - notificationStore.error(getAlertResourceIncidentNoteSaveFailure()); - } finally { - setIncidentNoteSaving((prev) => { - const next = new Set(prev); - next.delete(rowKey); - return next; - }); - } - }; - const bucketDurationLabel = createMemo(() => { const bucketHours = alertTrends().bucketSize; if (!Number.isFinite(bucketHours) || bucketHours <= 0) { @@ -1549,10 +1480,7 @@ export function HistoryTab(props: HistoryTabProps) { eventCardVariant="surface" noteDraft={incidentNoteDrafts()[rowKey] || ''} onNoteDraftChange={(value) => - setIncidentNoteDrafts((prev) => ({ - ...prev, - [rowKey]: value, - })) + setIncidentNoteDraft(rowKey, value) } noteSaving={incidentNoteSaving().has(rowKey)} onSaveNote={() => { diff --git a/frontend-modern/src/features/alerts/useAlertIncidentTimelineState.ts b/frontend-modern/src/features/alerts/useAlertIncidentTimelineState.ts new file mode 100644 index 0000000000..a48c6456f6 --- /dev/null +++ b/frontend-modern/src/features/alerts/useAlertIncidentTimelineState.ts @@ -0,0 +1,118 @@ +import { createSignal } from 'solid-js'; + +import { AlertsAPI } from '@/api/alerts'; +import { notificationStore } from '@/stores/notifications'; +import type { Incident } from '@/types/api'; +import { + getAlertResourceIncidentNoteSaveFailure, + getAlertResourceIncidentNoteSavedLabel, + getAlertResourceIncidentTimelineFailure, +} from '@/utils/alertIncidentPresentation'; +import { logger } from '@/utils/logger'; + +import { INCIDENT_EVENT_TYPES } from './types'; + +const createIncidentEventFilterSet = () => new Set(INCIDENT_EVENT_TYPES); + +export function useAlertIncidentTimelineState() { + const [incidentTimelines, setIncidentTimelines] = createSignal>( + {}, + ); + const [incidentLoading, setIncidentLoading] = createSignal>({}); + const [incidentErrors, setIncidentErrors] = createSignal>({}); + const [expandedIncidents, setExpandedIncidents] = createSignal>(new Set()); + const [incidentNoteDrafts, setIncidentNoteDrafts] = createSignal>({}); + const [incidentNoteSaving, setIncidentNoteSaving] = createSignal>(new Set()); + const [eventFilters, setEventFilters] = createSignal>(createIncidentEventFilterSet()); + + const resetState = () => { + setIncidentTimelines({}); + setIncidentLoading({}); + setIncidentErrors({}); + setExpandedIncidents(new Set()); + setIncidentNoteDrafts({}); + setIncidentNoteSaving(new Set()); + setEventFilters(createIncidentEventFilterSet()); + }; + + const loadIncidentTimeline = async (key: string, alertIdentifier: string, startedAt?: string) => { + setIncidentLoading((prev) => ({ ...prev, [key]: true })); + try { + const timeline = await AlertsAPI.getIncidentTimeline(alertIdentifier, startedAt); + setIncidentTimelines((prev) => ({ ...prev, [key]: timeline })); + setIncidentErrors((prev) => ({ ...prev, [key]: false })); + } catch (error) { + logger.error(getAlertResourceIncidentTimelineFailure(), error); + notificationStore.error(getAlertResourceIncidentTimelineFailure()); + setIncidentErrors((prev) => ({ ...prev, [key]: true })); + } finally { + setIncidentLoading((prev) => ({ ...prev, [key]: false })); + } + }; + + const toggleIncidentTimeline = async (key: string, alertIdentifier: string, startedAt?: string) => { + const next = new Set(expandedIncidents()); + if (next.has(key)) { + next.delete(key); + setExpandedIncidents(next); + return; + } + + next.add(key); + setExpandedIncidents(next); + + if (!(key in incidentTimelines())) { + await loadIncidentTimeline(key, alertIdentifier, startedAt); + } + }; + + const setIncidentNoteDraft = (key: string, value: string) => { + setIncidentNoteDrafts((prev) => ({ ...prev, [key]: value })); + }; + + const saveIncidentNote = async (key: string, alertIdentifier: string, startedAt?: string) => { + const note = (incidentNoteDrafts()[key] || '').trim(); + if (!note) { + return; + } + + setIncidentNoteSaving((prev) => { + const next = new Set(prev); + next.add(key); + return next; + }); + + try { + const incidentId = incidentTimelines()[key]?.id; + await AlertsAPI.addIncidentNote({ alertIdentifier, incidentId, note }); + setIncidentNoteDraft(key, ''); + await loadIncidentTimeline(key, alertIdentifier, startedAt); + notificationStore.success(getAlertResourceIncidentNoteSavedLabel()); + } catch (error) { + logger.error(getAlertResourceIncidentNoteSaveFailure(), error); + notificationStore.error(getAlertResourceIncidentNoteSaveFailure()); + } finally { + setIncidentNoteSaving((prev) => { + const next = new Set(prev); + next.delete(key); + return next; + }); + } + }; + + return { + incidentTimelines, + incidentLoading, + incidentErrors, + expandedIncidents, + incidentNoteDrafts, + incidentNoteSaving, + eventFilters, + setEventFilters, + resetState, + loadIncidentTimeline, + toggleIncidentTimeline, + setIncidentNoteDraft, + saveIncidentNote, + }; +} diff --git a/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts b/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts index 306baad128..df6a6c0383 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 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'; import alertScheduleTabSource from '@/features/alerts/tabs/ScheduleTab.tsx?raw'; @@ -191,9 +192,19 @@ describe('tab path helpers', () => { "import { HistoryTab } from '@/features/alerts/tabs/HistoryTab';", ); expect(alertsPageSource).not.toContain('function HistoryTab('); + expect(alertHistoryTabSource).toContain('useAlertIncidentTimelineState'); expect(alertDestinationsTabSource).toContain('NotificationsAPI.getWebhooks'); expect(alertHistoryTabSource).toContain('AlertsAPI.getHistory'); expect(alertHistoryTabSource).toContain('IncidentTimelinePanel'); + expect(alertHistoryTabSource).not.toContain('AlertsAPI.getIncidentTimeline'); + expect(alertHistoryTabSource).not.toContain('AlertsAPI.addIncidentNote'); + expect(alertHistoryTabSource).not.toContain('const loadIncidentTimeline = async'); + expect(alertHistoryTabSource).not.toContain('const saveIncidentNote = async'); + expect(alertIncidentTimelineStateSource).toContain( + 'export function useAlertIncidentTimelineState', + ); + expect(alertIncidentTimelineStateSource).toContain('AlertsAPI.getIncidentTimeline'); + expect(alertIncidentTimelineStateSource).toContain('AlertsAPI.addIncidentNote'); 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 4c8271ded1..c1b54725a8 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 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'; import alertScheduleTabSource from '@/features/alerts/tabs/ScheduleTab.tsx?raw'; @@ -2785,6 +2786,8 @@ describe('frontend resource type boundaries', () => { "import { HistoryTab } from '@/features/alerts/tabs/HistoryTab';", ); expect(alertsPageSource).not.toContain('function HistoryTab('); + expect(alertOverviewTabSource).toContain('useAlertIncidentTimelineState'); + expect(alertHistoryTabSource).toContain('useAlertIncidentTimelineState'); expect(alertHistoryTabSource).toContain('getAlertHistoryStatusPresentation'); expect(alertHistoryTabSource).toContain('getAlertHistorySourcePresentation'); expect(alertHistoryTabSource).toContain('getAlertHistoryResourceTypeBadgeClass'); @@ -2818,6 +2821,8 @@ describe('frontend resource type boundaries', () => { expect(alertHistoryTabSource).toContain('getAlertAdministrationClearHistoryLabel'); expect(alertHistoryTabSource).toContain('getAlertAdministrationClearHistoryError'); expect(alertHistoryTabSource).toContain('getAlertAdministrationClearHistoryConfirmation'); + expect(alertHistoryTabSource).not.toContain('AlertsAPI.getIncidentTimeline'); + expect(alertHistoryTabSource).not.toContain('AlertsAPI.addIncidentNote'); expect(alertsPageSource).toContain('getAlertActivationPresentation'); expect(alertsPageSource).toContain('getAlertActivationSuccess'); expect(alertsPageSource).toContain('getAlertActivationFailure'); @@ -3591,11 +3596,19 @@ 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(alertIncidentEventFiltersSource).toContain('getAlertIncidentEventFilterContainerClass'); expect(alertIncidentEventFiltersSource).toContain('getAlertIncidentEventFilterChipClass'); expect(alertIncidentEventFiltersSource).toContain( 'getAlertIncidentEventFilterActionButtonClass', ); + expect(alertIncidentTimelineStateSource).toContain( + 'export function useAlertIncidentTimelineState', + ); + expect(alertIncidentTimelineStateSource).toContain('AlertsAPI.getIncidentTimeline'); + expect(alertIncidentTimelineStateSource).toContain('AlertsAPI.addIncidentNote'); + expect(alertIncidentTimelineStateSource).toContain('getAlertResourceIncidentTimelineFailure'); + expect(alertIncidentTimelineStateSource).toContain('getAlertResourceIncidentNoteSavedLabel'); expect(alertIncidentTimelinePanelSource).toContain('getAlertTimelineLoadingState'); expect(alertIncidentTimelinePanelSource).toContain('getAlertTimelineFilterEmptyState'); expect(alertIncidentTimelinePanelSource).toContain('getAlertTimelineEmptyState'); @@ -3616,6 +3629,8 @@ describe('frontend resource type boundaries', () => { expect(alertOverviewTabSource).toContain('getAlertOverviewStartedAtClass'); expect(alertOverviewTabSource).toContain('getAlertOverviewPrimaryActionClass'); expect(alertOverviewTabSource).toContain('getAlertOverviewSecondaryActionClass'); + expect(alertOverviewTabSource).not.toContain('AlertsAPI.getIncidentTimeline'); + expect(alertOverviewTabSource).not.toContain('AlertsAPI.addIncidentNote'); expect(alertOverviewTabSource).not.toContain('Loading timeline...'); expect(alertOverviewTabSource).not.toContain('No timeline events match the selected filters.'); expect(alertOverviewTabSource).not.toContain('No timeline events yet.');