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 ;
+}