From d99f1f255df5564bcc6d10464134aa688f75e26a Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 22 Mar 2026 21:31:14 +0000 Subject: [PATCH] Split alert history table row owners --- .../v6/internal/subsystems/alerts.md | 4 +- .../subsystems/frontend-primitives.md | 4 +- .../alerts/AlertHistoryTableAlertRow.tsx | 180 ++++++++++++++ .../alerts/AlertHistoryTableGroupRow.tsx | 43 ++++ .../alerts/AlertHistoryTableSection.tsx | 228 +----------------- .../pages/__tests__/Alerts.helpers.test.ts | 13 +- .../frontendResourceTypeBoundaries.test.ts | 23 +- 7 files changed, 267 insertions(+), 228 deletions(-) create mode 100644 frontend-modern/src/features/alerts/AlertHistoryTableAlertRow.tsx create mode 100644 frontend-modern/src/features/alerts/AlertHistoryTableGroupRow.tsx diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index 68674584d..f62b106b6 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -292,7 +292,9 @@ detail now lives in `frontend-modern/src/features/alerts/AlertHistoryFrequencyCard.tsx`, `frontend-modern/src/features/alerts/AlertHistoryFiltersCard.tsx`, `frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx`, -`frontend-modern/src/features/alerts/AlertHistoryTableSection.tsx`, and +`frontend-modern/src/features/alerts/AlertHistoryTableSection.tsx`, +`frontend-modern/src/features/alerts/AlertHistoryTableGroupRow.tsx`, +`frontend-modern/src/features/alerts/AlertHistoryTableAlertRow.tsx`, and `frontend-modern/src/features/alerts/AlertHistoryAdministrationCard.tsx`. Future alert-history control-flow work should extend the feature hook, new grouping or trend semantics should extend the history model, and render-heavy diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 4713f0ea6..c640de833 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -554,7 +554,9 @@ render-heavy surfaces now route through `frontend-modern/src/features/alerts/AlertHistoryFrequencyCard.tsx`, `frontend-modern/src/features/alerts/AlertHistoryFiltersCard.tsx`, `frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx`, -`frontend-modern/src/features/alerts/AlertHistoryTableSection.tsx`, and +`frontend-modern/src/features/alerts/AlertHistoryTableSection.tsx`, +`frontend-modern/src/features/alerts/AlertHistoryTableGroupRow.tsx`, +`frontend-modern/src/features/alerts/AlertHistoryTableAlertRow.tsx`, and `frontend-modern/src/features/alerts/AlertHistoryAdministrationCard.tsx`. Future alert-history control flow should extend the hook, pure history analytics should extend the model, and section rendering should extend those owners diff --git a/frontend-modern/src/features/alerts/AlertHistoryTableAlertRow.tsx b/frontend-modern/src/features/alerts/AlertHistoryTableAlertRow.tsx new file mode 100644 index 000000000..21a1264ed --- /dev/null +++ b/frontend-modern/src/features/alerts/AlertHistoryTableAlertRow.tsx @@ -0,0 +1,180 @@ +import { Show } from 'solid-js'; + +import { IncidentTimelinePanel } from '@/components/Alerts/IncidentTimelinePanel'; +import { InvestigateAlertButton } from '@/components/Alerts/InvestigateAlertButton'; +import { TableCell, TableRow } from '@/components/shared/Table'; +import { + getAlertHistoryResourceTypeBadgeClass, + getAlertHistorySourcePresentation, +} from '@/utils/alertHistoryPresentation'; +import { + getAlertHistoryStatusPresentation, + getAlertIncidentLevelBadgeClass, + getAlertResourceIncidentViewTitle, +} from '@/utils/alertIncidentPresentation'; + +import type { AlertHistoryState } from './useAlertHistoryState'; + +type AlertHistoryAlert = ReturnType[number]['alerts'][number]; + +interface AlertHistoryTableAlertRowProps { + alert: AlertHistoryAlert; + state: AlertHistoryState; + hasAIAlertsFeature: () => boolean; + licenseLoading: () => boolean; +} + +export function AlertHistoryTableAlertRow(props: AlertHistoryTableAlertRowProps) { + const rowKey = () => props.state.getIncidentRowKey(props.alert); + const historyStatusPresentation = () => getAlertHistoryStatusPresentation(props.alert.status); + const sourcePresentation = () => getAlertHistorySourcePresentation(props.alert.source); + + return ( + <> + + + {new Date(props.alert.startTime).toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + })} + + + + {sourcePresentation().label} + + + + {props.alert.resourceName} + + + + + {props.alert.resourceType} + + + + + + {props.alert.severity} + + + + + {props.alert.description} + + + + {props.alert.duration} + + + + + {historyStatusPresentation().label} + + + + + {props.alert.nodeDisplayName || props.alert.node || '—'} + + + +
+ + + + + + + + + +
+
+
+ + + + + props.state.setIncidentNoteDraft(rowKey(), value)} + noteSaving={props.state.incidentNoteSaving().has(rowKey())} + onSaveNote={() => { + void props.state.saveIncidentNote(rowKey(), props.alert.id, props.alert.startTime); + }} + onRetry={() => { + void props.state.loadIncidentTimeline( + rowKey(), + props.alert.id, + props.alert.startTime, + ); + }} + /> + + + + + ); +} diff --git a/frontend-modern/src/features/alerts/AlertHistoryTableGroupRow.tsx b/frontend-modern/src/features/alerts/AlertHistoryTableGroupRow.tsx new file mode 100644 index 000000000..cdfb790fb --- /dev/null +++ b/frontend-modern/src/features/alerts/AlertHistoryTableGroupRow.tsx @@ -0,0 +1,43 @@ +import { TableCell, TableRow } from '@/components/shared/Table'; + +import type { AlertHistoryState } from './useAlertHistoryState'; + +type AlertHistoryGroup = ReturnType[number]; + +interface AlertHistoryTableGroupRowProps { + group: AlertHistoryGroup; +} + +function getGroupSummaryLabel(group: AlertHistoryGroup) { + const alertCount = group.alerts.filter((alert) => alert.source === 'alert').length; + const aiCount = group.alerts.filter((alert) => alert.source === 'ai').length; + const parts = []; + + if (alertCount > 0) { + parts.push(`${alertCount} alert${alertCount === 1 ? '' : 's'}`); + } + if (aiCount > 0) { + parts.push(`${aiCount} patrol insight${aiCount === 1 ? '' : 's'}`); + } + + return ( + parts.join(', ') || `${group.alerts.length} item${group.alerts.length === 1 ? '' : 's'}` + ); +} + +export function AlertHistoryTableGroupRow(props: AlertHistoryTableGroupRowProps) { + return ( + + +
+ + {props.group.label} + + + {getGroupSummaryLabel(props.group)} + +
+
+
+ ); +} diff --git a/frontend-modern/src/features/alerts/AlertHistoryTableSection.tsx b/frontend-modern/src/features/alerts/AlertHistoryTableSection.tsx index 33a5d5182..2a1290b98 100644 --- a/frontend-modern/src/features/alerts/AlertHistoryTableSection.tsx +++ b/frontend-modern/src/features/alerts/AlertHistoryTableSection.tsx @@ -1,11 +1,8 @@ import { For, Show } from 'solid-js'; -import { IncidentTimelinePanel } from '@/components/Alerts/IncidentTimelinePanel'; -import { InvestigateAlertButton } from '@/components/Alerts/InvestigateAlertButton'; import { Table, TableBody, - TableCell, TableHead, TableHeader, TableRow, @@ -14,17 +11,10 @@ import { getAlertHistoryEmptyState, getAlertHistoryLoadingState, } from '@/utils/alertOverviewPresentation'; -import { - getAlertHistoryResourceTypeBadgeClass, - getAlertHistorySourcePresentation, -} from '@/utils/alertHistoryPresentation'; -import { - getAlertHistoryStatusPresentation, - getAlertIncidentLevelBadgeClass, - getAlertResourceIncidentViewTitle, -} from '@/utils/alertIncidentPresentation'; import { getTypeColumnLabel } from '@/utils/typeColumnPresentation'; +import { AlertHistoryTableAlertRow } from './AlertHistoryTableAlertRow'; +import { AlertHistoryTableGroupRow } from './AlertHistoryTableGroupRow'; import type { AlertHistoryState } from './useAlertHistoryState'; interface AlertHistoryTableSectionProps { @@ -88,213 +78,17 @@ export function AlertHistoryTableSection(props: AlertHistoryTableSectionProps) { {(group) => ( <> - - -
- - {group.label} - - - {(() => { - const alertCount = group.alerts.filter( - (alert) => alert.source === 'alert', - ).length; - const aiCount = group.alerts.filter( - (alert) => alert.source === 'ai', - ).length; - const parts = []; - if (alertCount > 0) { - parts.push(`${alertCount} alert${alertCount === 1 ? '' : 's'}`); - } - if (aiCount > 0) { - parts.push( - `${aiCount} patrol insight${aiCount === 1 ? '' : 's'}`, - ); - } - return ( - parts.join(', ') || - `${group.alerts.length} item${group.alerts.length === 1 ? '' : 's'}` - ); - })()} - -
-
-
+ - {(alert) => { - const rowKey = props.state.getIncidentRowKey(alert); - const historyStatusPresentation = getAlertHistoryStatusPresentation( - alert.status, - ); - const sourcePresentation = getAlertHistorySourcePresentation( - alert.source, - ); - - return ( - <> - - - {new Date(alert.startTime).toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit', - })} - - - - - {sourcePresentation.label} - - - - - {alert.resourceName} - - - - - {alert.resourceType} - - - - - - {alert.severity} - - - - - {alert.description} - - - - {alert.duration} - - - - - {historyStatusPresentation.label} - - - - - {alert.nodeDisplayName || alert.node || '—'} - - - -
- - - - - - - - - -
-
-
- - - - - props.state.setIncidentNoteDraft(rowKey, value) - } - noteSaving={props.state.incidentNoteSaving().has(rowKey)} - onSaveNote={() => { - void props.state.saveIncidentNote( - rowKey, - alert.id, - alert.startTime, - ); - }} - onRetry={() => { - void props.state.loadIncidentTimeline( - rowKey, - alert.id, - alert.startTime, - ); - }} - /> - - - - - ); - }} + {(alert) => ( + + )}
)} diff --git a/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts b/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts index acb2262be..2573d4579 100644 --- a/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts +++ b/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts @@ -11,6 +11,8 @@ import alertAcknowledgementStateSource from '@/features/alerts/useAlertAcknowled import alertHistoryAdministrationCardSource from '@/features/alerts/AlertHistoryAdministrationCard.tsx?raw'; import alertHistoryFiltersCardSource from '@/features/alerts/AlertHistoryFiltersCard.tsx?raw'; import alertHistoryFrequencyCardSource from '@/features/alerts/AlertHistoryFrequencyCard.tsx?raw'; +import alertHistoryTableAlertRowSource from '@/features/alerts/AlertHistoryTableAlertRow.tsx?raw'; +import alertHistoryTableGroupRowSource from '@/features/alerts/AlertHistoryTableGroupRow.tsx?raw'; import alertHistoryTableSectionSource from '@/features/alerts/AlertHistoryTableSection.tsx?raw'; import alertResourceIncidentsPanelSource from '@/features/alerts/AlertResourceIncidentsPanel.tsx?raw'; import alertHistoryStateSource from '@/features/alerts/useAlertHistoryState.ts?raw'; @@ -315,8 +317,15 @@ describe('tab path helpers', () => { expect(alertResourceIncidentsPanelSource).toContain('IncidentEventFilters'); expect(alertResourceIncidentsPanelSource).toContain('IncidentTimelineEventCard'); expect(alertHistoryTableSectionSource).toContain('export function AlertHistoryTableSection'); - expect(alertHistoryTableSectionSource).toContain('IncidentTimelinePanel'); - expect(alertHistoryTableSectionSource).toContain('InvestigateAlertButton'); + expect(alertHistoryTableSectionSource).toContain('AlertHistoryTableGroupRow'); + expect(alertHistoryTableSectionSource).toContain('AlertHistoryTableAlertRow'); + expect(alertHistoryTableSectionSource).not.toContain('IncidentTimelinePanel'); + expect(alertHistoryTableSectionSource).not.toContain('InvestigateAlertButton'); + expect(alertHistoryTableGroupRowSource).toContain('export function AlertHistoryTableGroupRow'); + expect(alertHistoryTableGroupRowSource).toContain('getGroupSummaryLabel'); + expect(alertHistoryTableAlertRowSource).toContain('export function AlertHistoryTableAlertRow'); + expect(alertHistoryTableAlertRowSource).toContain('IncidentTimelinePanel'); + expect(alertHistoryTableAlertRowSource).toContain('InvestigateAlertButton'); expect(alertHistoryAdministrationCardSource).toContain( 'export function AlertHistoryAdministrationCard', ); diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index 9ddee88d8..c88e303f7 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -335,6 +335,8 @@ import alertAcknowledgementStateSource from '@/features/alerts/useAlertAcknowled import alertHistoryAdministrationCardSource from '@/features/alerts/AlertHistoryAdministrationCard.tsx?raw'; import alertHistoryFiltersCardSource from '@/features/alerts/AlertHistoryFiltersCard.tsx?raw'; import alertHistoryFrequencyCardSource from '@/features/alerts/AlertHistoryFrequencyCard.tsx?raw'; +import alertHistoryTableAlertRowSource from '@/features/alerts/AlertHistoryTableAlertRow.tsx?raw'; +import alertHistoryTableGroupRowSource from '@/features/alerts/AlertHistoryTableGroupRow.tsx?raw'; import alertHistoryTableSectionSource from '@/features/alerts/AlertHistoryTableSection.tsx?raw'; import alertResourceIncidentsPanelSource from '@/features/alerts/AlertResourceIncidentsPanel.tsx?raw'; import alertHistoryStateSource from '@/features/alerts/useAlertHistoryState.ts?raw'; @@ -2803,8 +2805,10 @@ describe('frontend resource type boundaries', () => { 'export function getDeployInstallCommandLoadingState', ); expect(deployStatusPresentationSource).toContain('export const getDeployStatusPresentation'); - expect(alertHistoryTableSectionSource).toContain('getAlertHistoryStatusPresentation'); - expect(alertHistoryTableSectionSource).toContain('getAlertIncidentLevelBadgeClass'); + expect(alertHistoryTableSectionSource).toContain('AlertHistoryTableGroupRow'); + expect(alertHistoryTableSectionSource).toContain('AlertHistoryTableAlertRow'); + expect(alertHistoryTableAlertRowSource).toContain('getAlertHistoryStatusPresentation'); + expect(alertHistoryTableAlertRowSource).toContain('getAlertIncidentLevelBadgeClass'); expect(alertsPageSource).toContain( "import { AlertsConfigurationSurface } from '@/features/alerts/AlertsConfigurationSurface';", ); @@ -2910,13 +2914,18 @@ describe('frontend resource type boundaries', () => { expect(alertResourceIncidentsPanelSource).not.toContain('getAlertIncidentTimelineEventCardClass'); expect(alertResourceIncidentsPanelSource).not.toContain('getAlertIncidentTimelineDetailClass'); expect(alertResourceIncidentsPanelSource).not.toContain('getAlertIncidentTimelineCommandClass'); - expect(alertHistoryTableSectionSource).toContain('getAlertHistoryStatusPresentation'); - expect(alertHistoryTableSectionSource).toContain('getAlertHistorySourcePresentation'); - expect(alertHistoryTableSectionSource).toContain('getAlertHistoryResourceTypeBadgeClass'); - expect(alertHistoryTableSectionSource).toContain('IncidentTimelinePanel'); - expect(alertHistoryTableSectionSource).toContain('InvestigateAlertButton'); + expect(alertHistoryTableSectionSource).toContain('AlertHistoryTableGroupRow'); + expect(alertHistoryTableSectionSource).toContain('AlertHistoryTableAlertRow'); expect(alertHistoryTableSectionSource).toContain('getAlertHistoryEmptyState'); expect(alertHistoryTableSectionSource).toContain('getAlertHistoryLoadingState'); + expect(alertHistoryTableSectionSource).not.toContain('IncidentTimelinePanel'); + expect(alertHistoryTableSectionSource).not.toContain('InvestigateAlertButton'); + expect(alertHistoryTableGroupRowSource).toContain('getGroupSummaryLabel'); + expect(alertHistoryTableAlertRowSource).toContain('getAlertHistoryStatusPresentation'); + expect(alertHistoryTableAlertRowSource).toContain('getAlertHistorySourcePresentation'); + expect(alertHistoryTableAlertRowSource).toContain('getAlertHistoryResourceTypeBadgeClass'); + expect(alertHistoryTableAlertRowSource).toContain('IncidentTimelinePanel'); + expect(alertHistoryTableAlertRowSource).toContain('InvestigateAlertButton'); expect(alertHistoryAdministrationCardSource).toContain('getAlertAdministrationSectionTitle'); expect(alertHistoryAdministrationCardSource).toContain( 'getAlertAdministrationSectionDescription',