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:
25
ui/src/components/AlertStateChip.test.tsx
Normal file
25
ui/src/components/AlertStateChip.test.tsx
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
27
ui/src/components/AlertStateChip.tsx
Normal file
27
ui/src/components/AlertStateChip.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
ui/src/components/SeverityBadge.test.tsx
Normal file
19
ui/src/components/SeverityBadge.test.tsx
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
20
ui/src/components/SeverityBadge.tsx
Normal file
20
ui/src/components/SeverityBadge.tsx
Normal 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" />;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user