From 31ee9748305e9e6719715f91ac364ea7a55992e0 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:21:37 +0200 Subject: [PATCH] feat(ui/alerts): AlertStateChip + SeverityBadge components State colors follow the convention from @cameleer/design-system (CRITICAL->error, WARNING->warning, INFO->auto). Silenced pill stacks next to state for the spec section 8 audit-trail surface. --- ui/src/components/AlertStateChip.test.tsx | 25 +++++++++++++++++++++ ui/src/components/AlertStateChip.tsx | 27 +++++++++++++++++++++++ ui/src/components/SeverityBadge.test.tsx | 19 ++++++++++++++++ ui/src/components/SeverityBadge.tsx | 20 +++++++++++++++++ 4 files changed, 91 insertions(+) create mode 100644 ui/src/components/AlertStateChip.test.tsx create mode 100644 ui/src/components/AlertStateChip.tsx create mode 100644 ui/src/components/SeverityBadge.test.tsx create mode 100644 ui/src/components/SeverityBadge.tsx diff --git a/ui/src/components/AlertStateChip.test.tsx b/ui/src/components/AlertStateChip.test.tsx new file mode 100644 index 00000000..dbdc0785 --- /dev/null +++ b/ui/src/components/AlertStateChip.test.tsx @@ -0,0 +1,25 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { ThemeProvider } from '@cameleer/design-system'; +import { AlertStateChip } from './AlertStateChip'; + +function renderWithTheme(ui: React.ReactElement) { + return render({ui}); +} + +describe('AlertStateChip', () => { + it.each([ + ['PENDING', /pending/i], + ['FIRING', /firing/i], + ['ACKNOWLEDGED', /acknowledged/i], + ['RESOLVED', /resolved/i], + ] as const)('renders %s label', (state, pattern) => { + renderWithTheme(); + expect(screen.getByText(pattern)).toBeInTheDocument(); + }); + + it('shows silenced suffix when silenced=true', () => { + renderWithTheme(); + expect(screen.getByText(/silenced/i)).toBeInTheDocument(); + }); +}); diff --git a/ui/src/components/AlertStateChip.tsx b/ui/src/components/AlertStateChip.tsx new file mode 100644 index 00000000..a81548f0 --- /dev/null +++ b/ui/src/components/AlertStateChip.tsx @@ -0,0 +1,27 @@ +import { Badge } from '@cameleer/design-system'; +import type { AlertDto } from '../api/queries/alerts'; + +type State = NonNullable; + +const LABELS: Record = { + PENDING: 'Pending', + FIRING: 'Firing', + ACKNOWLEDGED: 'Acknowledged', + RESOLVED: 'Resolved', +}; + +const COLORS: Record = { + PENDING: 'warning', + FIRING: 'error', + ACKNOWLEDGED: 'warning', + RESOLVED: 'success', +}; + +export function AlertStateChip({ state, silenced }: { state: State; silenced?: boolean }) { + return ( + + + {silenced && } + + ); +} diff --git a/ui/src/components/SeverityBadge.test.tsx b/ui/src/components/SeverityBadge.test.tsx new file mode 100644 index 00000000..a685fe12 --- /dev/null +++ b/ui/src/components/SeverityBadge.test.tsx @@ -0,0 +1,19 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { ThemeProvider } from '@cameleer/design-system'; +import { SeverityBadge } from './SeverityBadge'; + +function renderWithTheme(ui: React.ReactElement) { + return render({ui}); +} + +describe('SeverityBadge', () => { + it.each([ + ['CRITICAL', /critical/i], + ['WARNING', /warning/i], + ['INFO', /info/i], + ] as const)('renders %s', (severity, pattern) => { + renderWithTheme(); + expect(screen.getByText(pattern)).toBeInTheDocument(); + }); +}); diff --git a/ui/src/components/SeverityBadge.tsx b/ui/src/components/SeverityBadge.tsx new file mode 100644 index 00000000..b4d712ac --- /dev/null +++ b/ui/src/components/SeverityBadge.tsx @@ -0,0 +1,20 @@ +import { Badge } from '@cameleer/design-system'; +import type { AlertDto } from '../api/queries/alerts'; + +type Severity = NonNullable; + +const LABELS: Record = { + CRITICAL: 'Critical', + WARNING: 'Warning', + INFO: 'Info', +}; + +const COLORS: Record = { + CRITICAL: 'error', + WARNING: 'warning', + INFO: 'auto', +}; + +export function SeverityBadge({ severity }: { severity: Severity }) { + return ; +}