From a3c1e6bead44dae506bf7fa6cb10110cb9b1e42f Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 20 Mar 2026 14:30:44 +0000 Subject: [PATCH] Deduplicate alert incident timeline UI --- .../v6/internal/subsystems/alerts.md | 7 + .../subsystems/frontend-primitives.md | 6 + .../Alerts/IncidentEventFilters.tsx | 63 +++++ .../Alerts/IncidentTimelinePanel.tsx | 124 +++++++++ .../__tests__/IncidentEventFilters.test.tsx | 51 ++++ .../__tests__/IncidentTimelinePanel.test.tsx | 118 +++++++++ .../src/features/alerts/OverviewTab.tsx | 237 ++--------------- frontend-modern/src/features/alerts/types.ts | 5 +- frontend-modern/src/pages/Alerts.tsx | 247 +++--------------- .../pages/__tests__/Alerts.helpers.test.ts | 25 ++ .../frontendResourceTypeBoundaries.test.ts | 42 +-- 11 files changed, 476 insertions(+), 449 deletions(-) create mode 100644 frontend-modern/src/components/Alerts/IncidentEventFilters.tsx create mode 100644 frontend-modern/src/components/Alerts/IncidentTimelinePanel.tsx create mode 100644 frontend-modern/src/components/Alerts/__tests__/IncidentEventFilters.test.tsx create mode 100644 frontend-modern/src/components/Alerts/__tests__/IncidentTimelinePanel.test.tsx diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index d8cd45d6a..3d6c77050 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -119,6 +119,13 @@ route through `frontend-modern/src/utils/alertIncidentPresentation.ts` instead of keeping duplicate timeline card structure inline in the alerts page and overview timelines. +Expanded alert incident detail now also routes through +`frontend-modern/src/components/Alerts/IncidentTimelinePanel.tsx` and +`frontend-modern/src/components/Alerts/IncidentEventFilters.tsx` so the +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. + Resource incident panel cards, summary rows, and toggle-button presentation now also route through `frontend-modern/src/utils/alertIncidentPresentation.ts` instead of remaining inline inside `frontend-modern/src/pages/Alerts.tsx`. diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index af1a085c3..253d24236 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -144,6 +144,12 @@ Alert incident timeline event card structure must also route through alerts page and overview timeline share one canonical event-card renderer instead of reimplementing the same summary/detail/output block twice. +The full expanded alert incident detail panel and event-filter controls must +also route through `frontend-modern/src/components/Alerts/IncidentTimelinePanel.tsx` +and `frontend-modern/src/components/Alerts/IncidentEventFilters.tsx` rather +than rebuilding loading/error copy, filter controls, note-editor wiring, or +event-card composition separately inside the alerts page and overview tab. + Resource incident panel card and summary-row presentation must also route through `frontend-modern/src/utils/alertIncidentPresentation.ts` instead of maintaining page-local incident panel styling inside diff --git a/frontend-modern/src/components/Alerts/IncidentEventFilters.tsx b/frontend-modern/src/components/Alerts/IncidentEventFilters.tsx new file mode 100644 index 000000000..4f7d5971b --- /dev/null +++ b/frontend-modern/src/components/Alerts/IncidentEventFilters.tsx @@ -0,0 +1,63 @@ +import { For, type Accessor } from 'solid-js'; +import { INCIDENT_EVENT_LABELS, INCIDENT_EVENT_TYPES } from '@/features/alerts/types'; +import { + getAlertIncidentEventFilterActionButtonClass, + getAlertIncidentEventFilterChipClass, + getAlertIncidentEventFilterContainerClass, + getAlertIncidentEventFilterLabelClass, + type AlertIncidentEventFilterVariant, +} from '@/utils/alertIncidentPresentation'; + +export interface IncidentEventFiltersProps { + filters: Accessor>; + setFilters: (next: Set) => void; + variant: AlertIncidentEventFilterVariant; + showQuickSelection?: boolean; +} + +export function IncidentEventFilters(props: IncidentEventFiltersProps) { + const toggleFilter = (type: (typeof INCIDENT_EVENT_TYPES)[number]) => { + const next = new Set(props.filters()); + if (next.has(type)) { + next.delete(type); + } else { + next.add(type); + } + props.setFilters(next); + }; + + const label = () => (props.variant === 'panel' ? 'Filter events:' : 'Filters'); + + return ( +
+ {label()} + + {(action) => ( + + )} + + + {(type) => { + const selected = () => props.filters().has(type); + return ( + + ); + }} + +
+ ); +} diff --git a/frontend-modern/src/components/Alerts/IncidentTimelinePanel.tsx b/frontend-modern/src/components/Alerts/IncidentTimelinePanel.tsx new file mode 100644 index 000000000..c10b1b119 --- /dev/null +++ b/frontend-modern/src/components/Alerts/IncidentTimelinePanel.tsx @@ -0,0 +1,124 @@ +import { Show, For, createMemo, type Accessor } from 'solid-js'; +import type { Incident } from '@/types/api'; +import { filterIncidentEvents } from '@/features/alerts/types'; +import { IncidentEventFilters } from '@/components/Alerts/IncidentEventFilters'; +import { IncidentTimelineEventCard } from '@/components/Alerts/IncidentTimelineEventCard'; +import { + getAlertTimelineEmptyState, + getAlertTimelineFailureState, + getAlertTimelineFilterEmptyState, + getAlertTimelineLoadingState, + getAlertTimelineUnavailableState, +} from '@/utils/alertOverviewPresentation'; +import { + type AlertIncidentEventFilterVariant, + getAlertIncidentAcknowledgedBadgeClass, + getAlertIncidentNoteSaveButtonClass, + getAlertIncidentNoteTextareaClass, + getAlertIncidentTimelineHeadingClass, + getAlertIncidentTimelineMetaRowClass, + getAlertResourceIncidentNotePlaceholder, + getAlertResourceIncidentSaveNoteLabel, +} from '@/utils/alertIncidentPresentation'; + +export interface IncidentTimelinePanelProps { + timeline?: Incident | null; + loading: boolean; + error: boolean; + filters: Accessor>; + setFilters: (next: Set) => void; + filterVariant: AlertIncidentEventFilterVariant; + eventCardVariant: 'surface' | 'alt'; + noteDraft: string; + onNoteDraftChange: (value: string) => void; + noteSaving: boolean; + onSaveNote: () => void; + onRetry: () => void; +} + +export function IncidentTimelinePanel(props: IncidentTimelinePanelProps) { + const timeline = () => props.timeline; + const events = createMemo(() => timeline()?.events || []); + const filteredEvents = createMemo(() => filterIncidentEvents(events(), props.filters())); + + return ( + <> + +

{getAlertTimelineLoadingState().text}

+
+ + {(loadedTimeline) => ( +
+
+ Incident + {loadedTimeline().status} + + acknowledged + + + opened {new Date(loadedTimeline().openedAt).toLocaleString()} + + + closed {new Date(loadedTimeline().closedAt as string).toLocaleString()} + +
+ 0}> + + + 0}> +
+ + {(event) => ( + + )} + +
+
+ 0 && filteredEvents().length === 0}> +

{getAlertTimelineFilterEmptyState().text}

+
+ +

{getAlertTimelineEmptyState().text}

+
+
+