Extract alert incident timeline state owner

This commit is contained in:
rcourtman
2026-03-22 12:18:52 +00:00
parent d5dc1752a1
commit 683f2a8165
8 changed files with 268 additions and 164 deletions
@@ -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`
@@ -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
@@ -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<number>();
// Loading states for buttons
const [processingAlerts, setProcessingAlerts] = createSignal<Set<string>>(new Set());
const [incidentTimelines, setIncidentTimelines] = createSignal<Record<string, Incident | null>>(
{},
);
const [incidentLoading, setIncidentLoading] = createSignal<Record<string, boolean>>({});
const [incidentErrors, setIncidentErrors] = createSignal<Record<string, boolean>>({});
const [expandedIncidents, setExpandedIncidents] = createSignal<Set<string>>(new Set());
const [incidentNoteDrafts, setIncidentNoteDrafts] = createSignal<Record<string, string>>({});
const [incidentNoteSaving, setIncidentNoteSaving] = createSignal<Set<string>>(new Set());
const [incidentEventFilters, setIncidentEventFilters] = createSignal<Set<string>>(
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<string | null>(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={() => {
@@ -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);
});
});
@@ -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<Set<string>>(
new Set(),
);
const [historyIncidentEventFilters, setHistoryIncidentEventFilters] = createSignal<Set<string>>(
new Set(INCIDENT_EVENT_TYPES),
);
const [resourceIncidentEventFilters, setResourceIncidentEventFilters] = createSignal<Set<string>>(
new Set(INCIDENT_EVENT_TYPES),
);
const [incidentTimelines, setIncidentTimelines] = createSignal<Record<string, Incident | null>>(
{},
);
const [incidentLoading, setIncidentLoading] = createSignal<Record<string, boolean>>({});
const [incidentErrors, setIncidentErrors] = createSignal<Record<string, boolean>>({});
const [expandedIncidents, setExpandedIncidents] = createSignal<Set<string>>(new Set());
const [incidentNoteDrafts, setIncidentNoteDrafts] = createSignal<Record<string, string>>({});
const [incidentNoteSaving, setIncidentNoteSaving] = createSignal<Set<string>>(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<string>());
setIncidentTimelines({});
setIncidentLoading({});
setIncidentErrors({});
setExpandedIncidents(new Set<string>());
setIncidentNoteDrafts({});
setIncidentNoteSaving(new Set<string>());
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={() => {
@@ -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<string>(INCIDENT_EVENT_TYPES);
export function useAlertIncidentTimelineState() {
const [incidentTimelines, setIncidentTimelines] = createSignal<Record<string, Incident | null>>(
{},
);
const [incidentLoading, setIncidentLoading] = createSignal<Record<string, boolean>>({});
const [incidentErrors, setIncidentErrors] = createSignal<Record<string, boolean>>({});
const [expandedIncidents, setExpandedIncidents] = createSignal<Set<string>>(new Set());
const [incidentNoteDrafts, setIncidentNoteDrafts] = createSignal<Record<string, string>>({});
const [incidentNoteSaving, setIncidentNoteSaving] = createSignal<Set<string>>(new Set());
const [eventFilters, setEventFilters] = createSignal<Set<string>>(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,
};
}
@@ -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(
@@ -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.');