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.
This commit is contained in:
hsiegeln
2026-04-20 13:21:37 +02:00
parent 51bc796bec
commit 31ee974830
4 changed files with 91 additions and 0 deletions

View File

@@ -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(<ThemeProvider>{ui}</ThemeProvider>);
}
describe('AlertStateChip', () => {
it.each([
['PENDING', /pending/i],
['FIRING', /firing/i],
['ACKNOWLEDGED', /acknowledged/i],
['RESOLVED', /resolved/i],
] as const)('renders %s label', (state, pattern) => {
renderWithTheme(<AlertStateChip state={state} />);
expect(screen.getByText(pattern)).toBeInTheDocument();
});
it('shows silenced suffix when silenced=true', () => {
renderWithTheme(<AlertStateChip state="FIRING" silenced />);
expect(screen.getByText(/silenced/i)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,27 @@
import { Badge } from '@cameleer/design-system';
import type { AlertDto } from '../api/queries/alerts';
type State = NonNullable<AlertDto['state']>;
const LABELS: Record<State, string> = {
PENDING: 'Pending',
FIRING: 'Firing',
ACKNOWLEDGED: 'Acknowledged',
RESOLVED: 'Resolved',
};
const COLORS: Record<State, 'auto' | 'success' | 'warning' | 'error'> = {
PENDING: 'warning',
FIRING: 'error',
ACKNOWLEDGED: 'warning',
RESOLVED: 'success',
};
export function AlertStateChip({ state, silenced }: { state: State; silenced?: boolean }) {
return (
<span style={{ display: 'inline-flex', gap: 4, alignItems: 'center' }}>
<Badge label={LABELS[state]} color={COLORS[state]} variant="filled" />
{silenced && <Badge label="Silenced" color="auto" variant="outlined" />}
</span>
);
}

View File

@@ -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(<ThemeProvider>{ui}</ThemeProvider>);
}
describe('SeverityBadge', () => {
it.each([
['CRITICAL', /critical/i],
['WARNING', /warning/i],
['INFO', /info/i],
] as const)('renders %s', (severity, pattern) => {
renderWithTheme(<SeverityBadge severity={severity} />);
expect(screen.getByText(pattern)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,20 @@
import { Badge } from '@cameleer/design-system';
import type { AlertDto } from '../api/queries/alerts';
type Severity = NonNullable<AlertDto['severity']>;
const LABELS: Record<Severity, string> = {
CRITICAL: 'Critical',
WARNING: 'Warning',
INFO: 'Info',
};
const COLORS: Record<Severity, 'auto' | 'warning' | 'error'> = {
CRITICAL: 'error',
WARNING: 'warning',
INFO: 'auto',
};
export function SeverityBadge({ severity }: { severity: Severity }) {
return <Badge label={LABELS[severity]} color={COLORS[severity]} variant="filled" />;
}