diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index 533527914..d8cd45d6a 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -123,6 +123,13 @@ 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`. +That same resource incident panel now treats collapsed incident activity as a +canonical alert read-model summary rather than a page-local sentence. The +collapsed row must summarize filtered incident events by canonical event type +order and reuse the shared event-card renderer for expanded incident detail, +so the alert history page does not drift away from the overview timeline when +canonical lifecycle or remediation events are added. + Active alert card state, acknowledged badge, and primary/secondary action button presentation now route through `frontend-modern/src/utils/alertOverviewPresentation.ts` instead of remaining diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 720bf4e36..af1a085c3 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -149,6 +149,12 @@ through `frontend-modern/src/utils/alertIncidentPresentation.ts` instead of maintaining page-local incident panel styling inside `frontend-modern/src/pages/Alerts.tsx`. +The resource incident panel's collapsed activity summary is now part of that +same shared primitive boundary. Event-type count chips, visible-event copy, +and the summary-ordering helper in `frontend-modern/src/features/alerts/types.ts` +must stay shared across alert timeline surfaces instead of rebuilding +page-local event summaries or bespoke incident-card markup. + Shared primitive consumers that split status-dot tone and status-text tone must now keep both values routed through the same exported presentation helper. Feature cards such as RAID status may not call shadow local aliases that drift diff --git a/frontend-modern/src/features/alerts/types.ts b/frontend-modern/src/features/alerts/types.ts index 67f9bb8aa..34328532a 100644 --- a/frontend-modern/src/features/alerts/types.ts +++ b/frontend-modern/src/features/alerts/types.ts @@ -225,3 +225,46 @@ export const filterIncidentEvents = ( } return events.filter((event) => filters.has(event.type)); }; + +export interface IncidentEventSummary { + type: string; + label: string; + count: number; +} + +export const summarizeIncidentEvents = ( + events: IncidentEvent[] | undefined, +): IncidentEventSummary[] => { + if (!events || events.length === 0) { + return []; + } + + const counts = new Map(); + const unknownTypes: string[] = []; + + events.forEach((event) => { + counts.set(event.type, (counts.get(event.type) ?? 0) + 1); + if ( + !Object.prototype.hasOwnProperty.call(INCIDENT_EVENT_LABELS, event.type) && + !unknownTypes.includes(event.type) + ) { + unknownTypes.push(event.type); + } + }); + + const knownSummary = INCIDENT_EVENT_TYPES.flatMap((type) => { + const count = counts.get(type); + if (!count) { + return []; + } + return [{ type, label: INCIDENT_EVENT_LABELS[type], count }]; + }); + + const unknownSummary = unknownTypes.map((type) => ({ + type, + label: type, + count: counts.get(type) ?? 0, + })); + + return [...knownSummary, ...unknownSummary]; +}; diff --git a/frontend-modern/src/pages/Alerts.tsx b/frontend-modern/src/pages/Alerts.tsx index 6200b4799..3021371d4 100644 --- a/frontend-modern/src/pages/Alerts.tsx +++ b/frontend-modern/src/pages/Alerts.tsx @@ -78,6 +78,8 @@ import { getAlertIncidentTimelineHeadingClass, getAlertIncidentTimelineMetaRowClass, getAlertIncidentTimelineOutputClass, + getAlertResourceIncidentActivityChipClass, + getAlertResourceIncidentActivitySummaryClass, getAlertResourceIncidentAcknowledgedByLabel, getAlertResourceIncidentCardClass, getAlertResourceIncidentCountLabel, @@ -284,6 +286,7 @@ import { INCIDENT_EVENT_TYPES, GROUPING_WINDOW_DEFAULT_SECONDS, INCIDENT_EVENT_LABELS, + summarizeIncidentEvents, fallbackCooldownMinutes, clampCooldownMinutes, } from '@/features/alerts/types'; @@ -5005,6 +5008,7 @@ function HistoryTab(props: { events, resourceIncidentEventFilters(), ); + const eventSummary = summarizeIncidentEvents(filteredEvents); const recentEvents = filteredEvents.length > 6 ? filteredEvents.slice(filteredEvents.length - 6) @@ -5048,18 +5052,35 @@ function HistoryTab(props: { 0}>
- - 0} - fallback={ - - {getAlertResourceIncidentFilteredEventsEmptyState().text} - - } - > - Last event: {lastEvent?.summary} - - + 0} + fallback={ + + {getAlertResourceIncidentFilteredEventsEmptyState().text} + + } + > +
+ + Activity + + + {(summary) => ( + + {summary.label} {summary.count} + + )} + + + {filteredEvents.length !== events.length + ? `${filteredEvents.length}/${events.length} events` + : `${events.length} event${events.length === 1 ? '' : 's'}`} + + + Latest: {lastEvent?.summary} + +
+