diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index 7c034d6f76..24c7579b8d 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -1544,6 +1544,20 @@ presentation and sort below warnings, while the history facet exposes a real Info option whose count and filtered rows use the same predicate as every other severity. +The alert history frequency chart in `AlertHistoryFrequencyCard.tsx` stacks +each period by severity, critical at the base, from the `severityBuckets` that +`buildAlertTrends` counts in the same pass as its totals. Those counts use +`alertSeverityCountKey`, the closed vocabulary above: `error` is critical and +an empty or unknown value is a warning, never information. Segment fills and +legend dots share `getAlertSeverityFillClass`, each at least 3:1 against the +chart surface in both themes (light-theme warnings use amber-700). A bar's +height is the square root of its share of the busiest period (4px floor, 40px +ceiling), so a burst stands well clear while ordinary periods stay tall +enough to read their mix; each bar's tooltip and accessible name carry the +exact counts. The legend counts critical, warnings and info (info only when +present) over the current window and bar selection +(`AlertHistoryFrequencyCard.test.tsx`, `alertHistoryModel.test.ts`). + The desktop alert history table sizes each column for its content instead of giving all visible columns an equal share. Each density is sized for the narrowest card it serves (600px, 800px, 1024px and 1280px): Time (the column diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index ff8fda3b61..f03f35a19a 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -148,6 +148,14 @@ in alert helpers and the production held-breach browser fixture bind this shared presentation boundary to the alerts/API contracts. +### Alert severity fills are one primitive + +`getAlertSeverityFillClass` in `frontend-modern/src/utils/alertSeverityPresentation.ts` +is the one fill per alert severity, and `getAlertSeverityDotClass` composes +it, so a legend dot and the chart segment it names cannot drift apart. Each +fill holds at least 3:1 against the chart surface in both themes; yellow on +the light surface is under 2:1, so light-theme warnings use amber-700. + ### Systemd backup-pause help separates shutdown from remote completion The existing systemd precaution reads the stop result as well as the inactive diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index e1dc87e8d8..9050c7a387 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -2518,6 +2518,8 @@ "frontend-modern/src/components/Alerts/Thresholds/sections/__tests__/CollapsibleSection.test.tsx", "frontend-modern/src/components/Alerts/WebhookConfig.test.tsx", "frontend-modern/src/features/alerts/__tests__/AlertDeadManDestinationSection.test.tsx", + "frontend-modern/src/features/alerts/__tests__/AlertHistoryFrequencyCard.test.tsx", + "frontend-modern/src/features/alerts/__tests__/alertHistoryModel.test.ts", "frontend-modern/src/features/alerts/__tests__/AlertIncidentSynthesisSummary.test.tsx", "frontend-modern/src/features/alerts/__tests__/AlertIntentPolicyPanel.test.tsx", "frontend-modern/src/features/alerts/__tests__/AlertOverviewAlertCard.system-scope.test.tsx", diff --git a/frontend-modern/browser-verification/fa6da83d37342c34.json b/frontend-modern/browser-verification/fa6da83d37342c34.json new file mode 100644 index 0000000000..e12266984a --- /dev/null +++ b/frontend-modern/browser-verification/fa6da83d37342c34.json @@ -0,0 +1,42 @@ +{ + "version": 1, + "base_sha": "477a81e603792a131474e7c2f6b82ef197a99c11", + "verified_at": "2026-10-14T20:38:00Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/features/alerts/AlertHistoryFrequencyCard.tsx", + "frontend-modern/src/features/alerts/alertHistoryModel.ts", + "frontend-modern/src/utils/alertSeverityPresentation.ts" + ], + "content_sha256": { + "frontend-modern/src/features/alerts/AlertHistoryFrequencyCard.tsx": "453028f18aedc497bc485add31efc53ecf1776e19169739ab6a944a3445e2b78", + "frontend-modern/src/features/alerts/alertHistoryModel.ts": "a0d0ad493f06ac830ec45f588357aa6f76004be5b0a229a347bd13fbe9386491", + "frontend-modern/src/utils/alertSeverityPresentation.ts": "c9225ccb725fd476ab5504d3c63ba2db960be02028204dc7406b957424065317" + }, + "routes": [ + "/alerts/history" + ], + "viewports": [ + { + "width": 1280, + "height": 800 + }, + { + "width": 375, + "height": 812 + } + ], + "states": [ + "Before (main): every frequency bar one blue; legend showed only warnings and critical; 23 alerts drew 25px against 8px for one alert", + "After: each period stacks critical (base), warning and info in the legend colours; legend lists critical, warnings and info (info only when present)", + "Square-root heights: busiest period 40px, quiet periods 8-16px with their severity mix readable", + "Light theme: warning segments amber-700 (#b45309) on the slate-100 chart surface; dark theme keeps yellow-500", + "Phone 375px: chart scrolls inside its card, legend on one line, no horizontal page overflow" + ], + "interactions": [ + "Hovered a period: tooltip reads \"4 alerts: 2 critical, 1 warning, 1 info\" with the period and range", + "Selected a period: it is ringed, every other bar keeps full opacity, the list narrows to its alerts and the legend follows the selection", + "Switched light and dark themes and compared segment and legend colours", + "Repeated selection at 375x812 in dark theme" + ] +} diff --git a/frontend-modern/src/features/alerts/AlertHistoryFrequencyCard.tsx b/frontend-modern/src/features/alerts/AlertHistoryFrequencyCard.tsx index b323272de2..293b320e75 100644 --- a/frontend-modern/src/features/alerts/AlertHistoryFrequencyCard.tsx +++ b/frontend-modern/src/features/alerts/AlertHistoryFrequencyCard.tsx @@ -8,19 +8,53 @@ import { getAlertFrequencyClearFilterButtonClass, getAlertFrequencySelectionPresentation, } from '@/utils/alertFrequencyPresentation'; -import { getAlertSeverityDotClass } from '@/utils/alertSeverityPresentation'; +import { + getAlertSeverityDotClass, + getAlertSeverityFillClass, +} from '@/utils/alertSeverityPresentation'; -import { MS_PER_HOUR } from './alertHistoryModel'; +import { MS_PER_HOUR, alertSeverityCountKey, type AlertSeverityCounts } from './alertHistoryModel'; import type { AlertHistoryState } from './useAlertHistoryState'; interface AlertHistoryFrequencyCardProps { state: AlertHistoryState; } +// Most severe first: the legend reads in this order and each bar stacks it +// from the baseline up, so a period's critical alerts are never hidden. +const SEVERITY_ORDER = ['critical', 'warning', 'info'] as const; +const SEVERITY_NOUNS: Record<(typeof SEVERITY_ORDER)[number], [string, string]> = { + critical: ['critical', 'critical'], + warning: ['warning', 'warnings'], + info: ['info', 'info'], +}; + +const formatSeverityCount = (severity: (typeof SEVERITY_ORDER)[number], count: number) => + `${count} ${SEVERITY_NOUNS[severity][count === 1 ? 0 : 1]}`; + +const formatSeverityBreakdown = (counts: AlertSeverityCounts | undefined) => + counts + ? SEVERITY_ORDER.filter((severity) => counts[severity] > 0) + .map((severity) => formatSeverityCount(severity, counts[severity])) + .join(', ') + : ''; + +// The busiest period fills the chart and the rest scale by the square root of +// their share: a burst stands well clear of a quiet period (24 alerts against +// one is five times taller) while ordinary periods stay tall enough to read +// their severity mix. Each bar's tooltip carries the exact counts. +const BAR_MAX_PX = 40; +const BAR_MIN_PX = 4; + export function AlertHistoryFrequencyCard(props: AlertHistoryFrequencyCardProps) { const alertFrequencySelectionPresentation = createMemo(() => getAlertFrequencySelectionPresentation(), ); + const severityTotals = createMemo(() => { + const totals: AlertSeverityCounts = { critical: 0, warning: 0, info: 0 }; + for (const alert of props.state.alertData()) totals[alertSeverityCountKey(alert.severity)]++; + return totals; + }); return ( @@ -67,32 +101,25 @@ export function AlertHistoryFrequencyCard(props: AlertHistoryFrequencyCardProps) Clear filter -
- -
- { - props.state.alertData().filter((alert) => alert.severity === 'warning').length - }{' '} - warnings -
- -
- { - props.state.alertData().filter((alert) => alert.severity === 'critical').length - }{' '} - critical -
+
+ severity !== 'info' || severityTotals().info > 0, + )} + > + {(severity) => ( + + + {formatSeverityCount(severity, severityTotals()[severity])} + + )} +
-
- Showing {props.state.alertTrends().buckets.length} time periods ( - {props.state.bucketDurationLabel()} each) ยท Total: {props.state.alertData().length} alerts -
- {(() => { const trends = props.state.alertTrends(); return ( @@ -111,9 +138,15 @@ export function AlertHistoryFrequencyCard(props: AlertHistoryFrequencyCardProps)
{trends.buckets.map((value, index) => { - const scaledHeight = - value > 0 ? Math.min(100, Math.max(20, Math.log(value + 1) * 20)) : 0; - const pixelHeight = value > 0 ? Math.max(8, (scaledHeight / 100) * 40) : 0; + const pixelHeight = + value > 0 + ? Math.max( + BAR_MIN_PX, + Math.round(Math.sqrt(value / trends.max) * BAR_MAX_PX), + ) + : 0; + const counts = trends.severityBuckets?.[index]; + const breakdown = formatSeverityBreakdown(counts); const isSelected = () => props.state.selectedBarIndex() === index; const bucketStart = trends.bucketTimes[index]; const bucketEnd = bucketStart + trends.bucketSize * MS_PER_HOUR; @@ -124,7 +157,7 @@ export function AlertHistoryFrequencyCard(props: AlertHistoryFrequencyCardProps) : `${trends.bucketSize} hour${trends.bucketSize === 1 ? '' : 's'}`; const countLabel = getAlertBucketCountLabel(value); const tooltipContent = [ - countLabel, + breakdown ? `${countLabel}: ${breakdown}` : countLabel, `${bucketDurationText} period`, bucketRangeLabel, ].join('\n'); @@ -135,7 +168,7 @@ export function AlertHistoryFrequencyCard(props: AlertHistoryFrequencyCardProps) data-alert-frequency-bucket={index} class="relative flex h-12 min-w-6 flex-1 cursor-pointer items-end rounded-xs focus:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-inset" aria-pressed={isSelected()} - aria-label={`${countLabel} between ${bucketRangeLabel}`} + aria-label={`${breakdown ? `${countLabel} (${breakdown})` : countLabel} between ${bucketRangeLabel}`} onClick={() => props.state.setSelectedBarIndex( index === props.state.selectedBarIndex() ? null : index, @@ -144,16 +177,10 @@ export function AlertHistoryFrequencyCard(props: AlertHistoryFrequencyCardProps) > 0 ? (isSelected() ? '#2563eb' : '#3b82f6') : 'transparent', - opacity: isSelected() ? '1' : '0.8', - 'box-shadow': isSelected() - ? '0 0 0 2px rgba(37, 99, 235, 0.4)' - : 'none', - }} + data-alert-frequency-bar + class="relative flex w-full flex-col-reverse overflow-hidden rounded-xs" + classList={{ 'ring-2 ring-blue-500/60': isSelected() }} + style={{ height: `${pixelHeight}px` }} title={bucketRangeLabel} onMouseEnter={(event) => { if (value <= 0) { @@ -167,7 +194,21 @@ export function AlertHistoryFrequencyCard(props: AlertHistoryFrequencyCardProps) }); }} onMouseLeave={() => hideTooltip()} - /> + > + (counts?.[severity] ?? 0) > 0, + )} + > + {(severity) => ( + + )} + + ); })} diff --git a/frontend-modern/src/features/alerts/__tests__/AlertHistoryFrequencyCard.test.tsx b/frontend-modern/src/features/alerts/__tests__/AlertHistoryFrequencyCard.test.tsx index 441fcb5bf4..34fe780ba3 100644 --- a/frontend-modern/src/features/alerts/__tests__/AlertHistoryFrequencyCard.test.tsx +++ b/frontend-modern/src/features/alerts/__tests__/AlertHistoryFrequencyCard.test.tsx @@ -7,18 +7,28 @@ import type { AlertHistoryState } from '../useAlertHistoryState'; afterEach(cleanup); -function createFrequencyCardState(options?: { withAxis?: boolean }) { +const counts = (critical: number, warning: number, info: number) => ({ critical, warning, info }); + +function createFrequencyCardState(options?: { + withAxis?: boolean; + buckets?: number[]; + severityBuckets?: ReturnType[]; + severities?: string[]; +}) { const [selectedBarIndex, setSelectedBarIndex] = createSignal(null); + const buckets = options?.buckets ?? [0, 1, 2]; const state = { - alertData: () => [], + alertData: () => (options?.severities ?? []).map((severity) => ({ severity })), selectedBucketDetails: () => null, bucketDurationLabel: () => '1 hour', rangeSummary: () => null, selectedBarIndex, setSelectedBarIndex, alertTrends: () => ({ - buckets: [0, 1, 2], - bucketTimes: [0, 60 * 60 * 1000, 2 * 60 * 60 * 1000], + buckets, + severityBuckets: options?.severityBuckets ?? buckets.map((value) => counts(0, value, 0)), + max: Math.max(...buckets, 1), + bucketTimes: buckets.map((_, index) => index * 60 * 60 * 1000), bucketSize: 1, }), formatBucketRange: (start: number) => `Period ${start}`, @@ -85,4 +95,78 @@ describe('AlertHistoryFrequencyCard', () => { expect(axisTicks).toHaveLength(2); expect(axisTicks.every((tick) => scrollSurface!.contains(tick))).toBe(true); }); + it('stacks each period by severity in the legend colours, critical at the base', () => { + const { state } = createFrequencyCardState({ + buckets: [3, 0], + severityBuckets: [counts(1, 2, 0), counts(0, 0, 0)], + }); + const { container } = render(() => ); + + const bar = container.querySelector( + '[data-alert-frequency-bucket="0"] [data-alert-frequency-bar]', + ); + const segments = Array.from( + bar!.querySelectorAll('[data-alert-frequency-segment]'), + ); + // The bar stacks from the bottom (flex-col-reverse), so the first segment + // sits on the baseline. + expect(bar).toHaveClass('flex-col-reverse'); + expect(segments.map((segment) => segment.dataset.alertFrequencySegment)).toEqual([ + 'critical', + 'warning', + ]); + expect(segments[0]).toHaveClass('bg-red-500'); + expect(segments[1]).toHaveClass('bg-amber-700', 'dark:bg-yellow-500'); + expect(segments[0].style.height).toBe(`${(1 / 3) * 100}%`); + expect( + container.querySelectorAll( + '[data-alert-frequency-bucket="1"] [data-alert-frequency-segment]', + ), + ).toHaveLength(0); + + const button = container.querySelector('[data-alert-frequency-bucket="0"]'); + expect(button?.getAttribute('aria-label')).toContain('3 alerts (1 critical, 2 warnings)'); + }); + + it('scales bars from the busiest period so a burst stands well clear', () => { + const { state } = createFrequencyCardState({ buckets: [1, 4, 25, 0] }); + const { container } = render(() => ); + const heights = Array.from( + container.querySelectorAll('[data-alert-frequency-bar]'), + ).map((bar) => bar.style.height); + + // Square root of each period's share of the busiest: 25 alerts read five + // times taller than one (the old log scale drew 8px, 13px and 26px, so a + // 25-alert burst looked barely three times one alert). + expect(heights).toEqual(['8px', '16px', '40px', '0px']); + }); + + it('counts every severity in the legend, info only when present', () => { + const { state } = createFrequencyCardState({ + severities: ['critical', 'warning', 'warning', 'info', 'Critical'], + }); + const { container } = render(() => ); + const legend = container.querySelector('[data-alert-frequency-legend]'); + expect(legend?.textContent).toBe('2 critical2 warnings1 info'); + + cleanup(); + const quiet = createFrequencyCardState({ severities: ['warning'] }); + const view = render(() => ); + expect(view.container.querySelector('[data-alert-frequency-legend]')?.textContent).toBe( + '0 critical1 warning', + ); + }); + + it('rings the selected period and keeps every other bar at full strength', () => { + const { state } = createFrequencyCardState(); + const { container } = render(() => ); + fireEvent.click(container.querySelector('[data-alert-frequency-bucket="2"]')!); + + // Dimming the rest would drop their fills under 3:1 on the light surface, + // and they still carry severity and remain clickable. + const bars = Array.from(container.querySelectorAll('[data-alert-frequency-bar]')); + expect(bars[2]).toHaveClass('ring-2'); + expect(bars[1]).not.toHaveClass('ring-2'); + for (const bar of bars) expect(bar.className).not.toMatch(/opacity-/); + }); }); diff --git a/frontend-modern/src/features/alerts/__tests__/alertHistoryModel.test.ts b/frontend-modern/src/features/alerts/__tests__/alertHistoryModel.test.ts index 58aa21e381..7e58376b64 100644 --- a/frontend-modern/src/features/alerts/__tests__/alertHistoryModel.test.ts +++ b/frontend-modern/src/features/alerts/__tests__/alertHistoryModel.test.ts @@ -99,6 +99,28 @@ describe('alertHistoryModel', () => { expect(details?.rangeLabel).toContain('Mar'); }); + it('counts each period by severity alongside its total', () => { + const alerts = [ + { id: 'a', startTime: '2026-03-22T09:05:00.000Z', severity: 'critical' }, + { id: 'b', startTime: '2026-03-22T09:10:00.000Z', severity: 'warning' }, + { id: 'c', startTime: '2026-03-22T09:20:00.000Z', severity: 'Warning' }, + { id: 'd', startTime: '2026-03-22T09:40:00.000Z', severity: 'info' }, + { id: 'e', startTime: '2026-03-22T08:10:00.000Z' }, + { id: 'f', startTime: '2026-03-22T08:20:00.000Z', severity: 'error' }, + ]; + const trends = buildAlertTrends(alerts as any, '24h', Date.UTC(2026, 2, 22, 10, 0, 0)); + const nine = trends.buckets.length - 1; + + expect(trends.buckets[nine]).toBe(4); + expect(trends.severityBuckets[nine]).toEqual({ critical: 1, warning: 2, info: 1 }); + // A missing severity fails safe to warning; legacy `error` is critical. + expect(trends.severityBuckets[nine - 1]).toEqual({ critical: 1, warning: 1, info: 0 }); + trends.buckets.forEach((total, index) => { + const counts = trends.severityBuckets[index]; + expect(counts.critical + counts.warning + counts.info).toBe(total); + }); + }); + it('keeps a stable id order for alerts sharing a startTime (#1218)', () => { const sharedStart = '2026-03-22T09:00:00.000Z'; const items = [ diff --git a/frontend-modern/src/features/alerts/alertHistoryModel.ts b/frontend-modern/src/features/alerts/alertHistoryModel.ts index 0be5a89f2b..0ddb250cf3 100644 --- a/frontend-modern/src/features/alerts/alertHistoryModel.ts +++ b/frontend-modern/src/features/alerts/alertHistoryModel.ts @@ -61,6 +61,28 @@ export interface AlertTrendSeries { rangeHours: number; } +export interface AlertSeverityCounts { + critical: number; + warning: number; + info: number; +} + +/** + * The frequency chart's series: each period's alerts by severity as well as + * in total, so a busy period shows how bad it was. + */ +export interface AlertFrequencySeries extends AlertTrendSeries { + severityBuckets: AlertSeverityCounts[]; +} + +// The closed alert vocabulary: legacy `error` is critical, and an empty or +// unknown value fails safe to warning rather than reading as information. +export const alertSeverityCountKey = (severity: string | undefined): keyof AlertSeverityCounts => { + const normalized = (severity ?? '').trim().toLowerCase(); + if (normalized === 'critical' || normalized === 'error') return 'critical'; + return normalized === 'info' ? 'info' : 'warning'; +}; + export interface AlertAxisTick { position: number; label: string; @@ -326,7 +348,7 @@ export function buildAlertTrends( filteredAlerts: HistoryItem[], timeFilter: AlertHistoryRange, now = Date.now(), -): AlertTrendSeries { +): AlertFrequencySeries { const niceBucketSizes = [1, 2, 3, 6, 12, 24, 48, 72, 168, 336, 720, 1440]; const maxBuckets = 30; @@ -373,6 +395,11 @@ export function buildAlertTrends( startTime = Math.min(startTime, now - bucketCount * bucketSizeHours * MS_PER_HOUR); const buckets = new Array(bucketCount).fill(0); + const severityBuckets: AlertSeverityCounts[] = Array.from({ length: bucketCount }, () => ({ + critical: 0, + warning: 0, + info: 0, + })); const bucketTimes = new Array(bucketCount) .fill(0) .map((_, index) => startTime + index * bucketSizeHours * MS_PER_HOUR); @@ -388,11 +415,13 @@ export function buildAlertTrends( const bucketIndex = Math.min(bucketCount - 1, Math.max(0, rawIndex)); if (bucketIndex >= 0 && bucketIndex < bucketCount) { buckets[bucketIndex]++; + severityBuckets[bucketIndex][alertSeverityCountKey(alert.severity)]++; } }); return { buckets, + severityBuckets, max: Math.max(...buckets, 1), bucketSize: bucketSizeHours, bucketTimes, diff --git a/frontend-modern/src/utils/__tests__/alertSeverityPresentation.test.ts b/frontend-modern/src/utils/__tests__/alertSeverityPresentation.test.ts index 712568e1fc..3df541c7b8 100644 --- a/frontend-modern/src/utils/__tests__/alertSeverityPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/alertSeverityPresentation.test.ts @@ -6,6 +6,7 @@ import { getAlertSeverityBadgeClass, getAlertSeverityCompactLabel, getAlertSeverityDotClass, + getAlertSeverityFillClass, getAlertSeverityIndicatorVariant, getAlertSeverityTextClass, } from '@/utils/alertSeverityPresentation'; @@ -60,10 +61,19 @@ describe('alertSeverityPresentation', () => { it('maps severity dots to canonical colors', () => { expect(getAlertSeverityDotClass('critical')).toBe('h-2 w-2 rounded-full bg-red-500'); - expect(getAlertSeverityDotClass('warning')).toBe('h-2 w-2 rounded-full bg-yellow-500'); + expect(getAlertSeverityDotClass('warning')).toBe( + 'h-2 w-2 rounded-full bg-amber-700 dark:bg-yellow-500', + ); expect(getAlertSeverityDotClass('info')).toBe('h-2 w-2 rounded-full bg-blue-500'); }); + it('fills a severity the same colour as its dot, so a chart segment matches its legend', () => { + for (const level of ['critical', 'warning', 'info', 'unknown']) { + expect(getAlertSeverityDotClass(level)).toContain(getAlertSeverityFillClass(level)); + } + expect(getAlertSeverityFillClass('CRITICAL')).toBe('bg-red-500'); + }); + it('maps compact severity labels canonically', () => { expect(getAlertSeverityCompactLabel('critical')).toBe('CRIT'); expect(getAlertSeverityCompactLabel('warning')).toBe('WARN'); diff --git a/frontend-modern/src/utils/alertSeverityPresentation.ts b/frontend-modern/src/utils/alertSeverityPresentation.ts index 058828efb2..9409a34309 100644 --- a/frontend-modern/src/utils/alertSeverityPresentation.ts +++ b/frontend-modern/src/utils/alertSeverityPresentation.ts @@ -95,13 +95,20 @@ export function getAlertSeverityCompactLabel(level: Alert['level'] | string): st return ALERT_SEVERITY_COMPACT_LABELS[normalized] || String(level).toUpperCase(); } -export function getAlertSeverityDotClass(level: Alert['level'] | string): string { +// One fill per severity, so a legend dot and the chart segment it names match. +// Each holds at least 3:1 against the chart's surface in both themes; yellow +// on the light surface is under 2:1, so light-theme warnings use amber-700. +export function getAlertSeverityFillClass(level: Alert['level'] | string): string { switch (normalizeAlertSeverity(level)) { case 'critical': - return 'h-2 w-2 rounded-full bg-red-500'; + return 'bg-red-500'; case 'warning': - return 'h-2 w-2 rounded-full bg-yellow-500'; + return 'bg-amber-700 dark:bg-yellow-500'; default: - return 'h-2 w-2 rounded-full bg-blue-500'; + return 'bg-blue-500'; } } + +export function getAlertSeverityDotClass(level: Alert['level'] | string): string { + return `h-2 w-2 rounded-full ${getAlertSeverityFillClass(level)}`; +} diff --git a/scripts/release_control/canonical_completion_guard_test.py b/scripts/release_control/canonical_completion_guard_test.py index 3fe826e9cf..ab859a4dd2 100644 --- a/scripts/release_control/canonical_completion_guard_test.py +++ b/scripts/release_control/canonical_completion_guard_test.py @@ -2486,6 +2486,7 @@ None yet. "frontend-modern/src/components/Alerts/__tests__/ThresholdsTable.test.tsx", "frontend-modern/src/features/alerts/AlertDeliveryHealthCard.test.tsx", "frontend-modern/src/features/alerts/__tests__/AlertDeadManDestinationSection.test.tsx", + "frontend-modern/src/features/alerts/__tests__/AlertHistoryFrequencyCard.test.tsx", "frontend-modern/src/features/alerts/__tests__/AlertIncidentSynthesisSummary.test.tsx", "frontend-modern/src/features/alerts/__tests__/AlertIntentPolicyPanel.test.tsx", "frontend-modern/src/features/alerts/__tests__/AlertOverviewAlertCard.system-scope.test.tsx", @@ -2493,6 +2494,7 @@ None yet. "frontend-modern/src/features/alerts/__tests__/OverviewTab.timelineerror.test.tsx", "frontend-modern/src/features/alerts/__tests__/OverviewTab.total24h.test.tsx", "frontend-modern/src/features/alerts/__tests__/ThresholdsTab.test.tsx", + "frontend-modern/src/features/alerts/__tests__/alertHistoryModel.test.ts", "frontend-modern/src/features/alerts/__tests__/alertsConfigurationModel.test.ts", "frontend-modern/src/features/alerts/__tests__/helpers.test.ts", "frontend-modern/src/features/alerts/__tests__/useAlertDestinationsState.test.tsx",