docs(alerts): Design spec — design-system alignment for /alerts pages

Rework all pages under /alerts to use @cameleer/design-system components
and tokens. Unified DataTable shell for Inbox/All/History with expandable
rows; DataTable + Dropdown + ConfirmDialog for Rules list; FormField grid
+ DataTable for Silences; DS Alert for wizard banners. Replaces undefined
CSS variables (--bg, --fg, --muted, --accent) with DS tokens and removes
raw <table>/<select>/confirm() usage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-21 09:43:19 +02:00
parent 037a27d405
commit 3d0a4d289b

View File

@@ -0,0 +1,180 @@
# Alerts pages — design-system alignment
**Status:** Approved (2026-04-21)
**Scope:** All pages and helper components under `/alerts` in `ui/src/pages/Alerts/` plus `ui/src/components/NotificationBell.tsx` (audit only — already on DS).
**Non-goals:** Backend changes, DS package changes, alert semantics, `MustacheEditor` restyling.
## Problem
Pages under `/alerts` don't fully adhere to the `@cameleer/design-system` styling and component conventions used by the rest of the SPA (Admin, Audit, Apps, Runtime). Concretely:
1. **Undefined CSS variables.** `alerts-page.module.css` and `wizard.module.css` use tokens (`--bg`, `--fg`, `--muted`, `--accent`) that are **not** defined by the DS (verified against `@cameleer/design-system/dist/style.css`). These fall back to browser defaults and do not theme correctly in dark mode. (Note: `--border` and `--amber-bg` **are** valid DS tokens, but `--border-subtle` is the convention used by the rest of the app for card chrome.)
2. **Raw HTML where DS components exist.** Raw `<table>` (RulesList, Silences), raw `<select>` (RulesList promote), custom centered-div empty states, custom "promote banner" div.
3. **Inconsistent page layout.** Toolbars built ad-hoc with inline styles. Admin / Audit pages use a consistent `SectionHeader + sectionStyles.section / tableStyles.tableSection` shell.
4. **Native `confirm()`** instead of DS `ConfirmDialog`.
## Design principles
1. **Consistency over novelty** — all three list pages (Inbox / All / History) share one `DataTable` shell; they differ only in toolbar controls.
2. **Double-encode severity** — DS `SeverityBadge` column **and** `rowAccent` tint — accessible to colorblind users.
3. **Expandable rows** give the inbox-style preview affordance without needing a separate feed layout.
4. **Relative time** (`2m ago`) with tooltip for absolute ISO — industry-standard for alert consoles.
5. **Use DS tokens only**`--bg-surface`, `--border-subtle`, `--radius-lg`, `--shadow-card`, `--text-primary/secondary/muted`, `--space-sm/md/lg`.
## Per-page design
### Inbox (`/alerts/inbox`)
Personal triage queue — user-targeted FIRING/ACKNOWLEDGED alerts.
- Shell: `<SectionHeader>Inbox</SectionHeader>` → bulk-action toolbar (`Mark selected read`, `Mark all read`) → `tableStyles.tableSection` wrapping `DataTable`.
- Columns: **☐ checkbox | Severity | State | Title | App/Rule | Age | Ack**.
- `rowAccent`: map severity → `error | warning | info`. Unread (FIRING) rows render with DataTable's inherent accent tint; additional bold weight on title via `render`.
- `expandedContent`: message body, targeted users, fireMode, absolute firedAt/updatedAt.
- Empty state: DS `<EmptyState icon={<Inbox />} title="All clear" description="No open alerts for you in this environment." />`.
### All alerts (`/alerts/all`)
Env-wide operational awareness.
- Same shell as Inbox, minus the checkbox column.
- Filter bar: DS `ButtonGroup` with items `Open` / `Firing` / `Acked` / `All`. Replaces the current four-`Button` row.
- Columns: **Severity | State | Title | App/Rule | Fired at | Silenced**.
- `expandedContent`: same as Inbox.
- Empty state: `EmptyState` with filter-specific message.
### History (`/alerts/history`)
Retrospective lookup — RESOLVED alerts only.
- Same shell as All.
- Filter bar: DS `DateRangePicker` (default: last 7 days). Replaces the static "retention window" label.
- Columns: **Severity | Title | App/Rule | Fired at | Resolved at | Duration**.
- `expandedContent`: message body, rule snapshot pointer, full timestamps.
- Empty state: `EmptyState` with "No resolved alerts in selected range."
### Rules list (`/alerts/rules`)
- Shell: `<SectionHeader action={<Button>New rule</Button>}>` with DS `action` slot — replaces the inline flex container that currently wraps them.
- Raw `<table>``DataTable` inside `tableStyles.tableSection`.
- Columns: **Name (Link) | Kind (Badge) | Severity (SeverityBadge) | Enabled (Toggle) | Targets (count) | Actions**.
- Actions cell: DS `Dropdown` for **Promote to env** (replaces raw `<select>`), DS `Button variant="ghost"` **Delete** opening a `ConfirmDialog`.
- Empty state: `EmptyState` with CTA linking to `/alerts/rules/new`.
### Silences (`/alerts/silences`)
- Shell: `<SectionHeader>Alert silences</SectionHeader>`.
- Create form: kept in `sectionStyles.section`, but grid laid out via `FormField`s with proper `Label` and `hint` props — no inline-style grid.
- List: raw `<table>``DataTable` below the form.
- Columns: **Matcher (MonoText) | Reason | Starts | Ends | End action**.
- `End` action → `ConfirmDialog`.
- Empty state: `EmptyState` "No active or scheduled silences."
### Rule editor wizard (`/alerts/rules/new`, `/alerts/rules/:id`)
Keep the current custom tab stepper — DS has no `Stepper`, and the existing layout is appropriate.
Changes:
- `wizard.module.css` — replace undefined tokens with DS tokens. `.wizard` uses `--space-md` gap; `.steps` underline uses `--border-subtle`; `.stepActive` border uses `--amber` (the DS accent color); `.step` idle color uses `--text-muted`, active/done uses `--text-primary`.
- Promote banner → DS `<Alert variant="info">`.
- Warnings block → DS `<Alert variant="warning">` with the list as children.
- Step body wraps in `sectionStyles.section` for card affordance matching other forms.
## Shared changes
### `alerts-page.module.css`
Reduced to layout-only:
```css
.page {
padding: var(--space-md);
display: flex;
flex-direction: column;
gap: var(--space-md);
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-sm);
}
.filterBar {
display: flex;
gap: var(--space-sm);
align-items: center;
}
```
Delete: `.row`, `.rowUnread`, `.body`, `.meta`, `.time`, `.message`, `.actions`, `.empty` — all replaced by DS components.
### `AlertRow.tsx`
**Delete.** Logic migrates into:
- A column renderer for the Title cell (handles the `Link` + `markRead` side-effect on click).
- An `expandedContent` renderer shared across the three list pages (extracted into `ui/src/pages/Alerts/alert-expanded.tsx`).
- An Ack action button rendered via DataTable `Actions` column.
### `ConfirmDialog` migration
Replaces native `confirm()` in `RulesListPage` (delete), `SilencesPage` (end), and the wizard if it grows a delete path (not currently present).
### Helpers
Two small pure-logic helpers, co-located in `ui/src/pages/Alerts/`:
- `time-utils.ts``formatRelativeTime(iso: string, now?: Date): string` returning `2m ago` / `1h ago` / `3d ago`. With a Vitest.
- `severity-utils.ts``severityToAccent(severity: AlertSeverity): DataTableRowAccent` mapping `CRITICAL→error`, `MAJOR→warning`, `MINOR→warning`, `INFO→info`. With a Vitest.
## Components touched
| File | Change |
|------|--------|
| `ui/src/pages/Alerts/InboxPage.tsx` | Rewrite: DataTable + bulk toolbar + EmptyState |
| `ui/src/pages/Alerts/AllAlertsPage.tsx` | Rewrite: DataTable + ButtonGroup filter + EmptyState |
| `ui/src/pages/Alerts/HistoryPage.tsx` | Rewrite: DataTable + DateRangePicker + EmptyState |
| `ui/src/pages/Alerts/RulesListPage.tsx` | Table → DataTable; select → Dropdown; confirm → ConfirmDialog |
| `ui/src/pages/Alerts/SilencesPage.tsx` | Table → DataTable; FormField grid; confirm → ConfirmDialog |
| `ui/src/pages/Alerts/AlertRow.tsx` | **Delete** |
| `ui/src/pages/Alerts/alert-expanded.tsx` | **New** — shared expandedContent renderer |
| `ui/src/pages/Alerts/time-utils.ts` | **New** |
| `ui/src/pages/Alerts/time-utils.test.ts` | **New** |
| `ui/src/pages/Alerts/severity-utils.ts` | **New** |
| `ui/src/pages/Alerts/severity-utils.test.ts` | **New** |
| `ui/src/pages/Alerts/alerts-page.module.css` | Slim down to layout-only, DS tokens |
| `ui/src/pages/Alerts/RuleEditor/wizard.module.css` | Replace legacy tokens → DS tokens |
| `ui/src/pages/Alerts/RuleEditor/RuleEditorWizard.tsx` | Promote banner / warnings → DS `Alert`; step body → section-card wrap |
## Testing
1. **Unit (Vitest):**
- `time-utils.test.ts` — relative time formatting at 0s / 30s / 5m / 2h / 3d / 10d boundaries.
- `severity-utils.test.ts` — all four severities map correctly.
2. **Component tests:**
- Existing `AlertStateChip.test.tsx`, `SeverityBadge.test.tsx` keep passing (no change).
- `NotificationBell.test.tsx` — unchanged (this component already uses DS correctly per audit).
3. **E2E (Playwright):**
- Inbox empty state renders.
- AllAlerts filter ButtonGroup switches active state and requery fires.
- Rules list delete opens ConfirmDialog, confirms, row disappears.
- Wizard promote banner renders as `Alert`.
4. **Manual smoke:**
- Light + dark theme on all five pages — verify no raw `<table>` borders bleeding through; all surfaces use DS card shadows.
- Screenshot comparison pre/post via already-present Playwright config.
## Open questions
None — DS v0.1.56 ships every primitive we need (`DataTable`, `EmptyState`, `Alert`, `ButtonGroup`, `Dropdown`, `ConfirmDialog`, `DateRangePicker`, `FormField`, `MonoText`). If a gap surfaces during implementation, flag it as a separate DS-change discussion per user's standing rule.
## Out of scope
- Keyboard shortcuts (j/k nav, e to ack) — future enhancement.
- Grouping alerts by rule (collapse duplicates) — future enhancement.
- `MustacheEditor` visual restyling — separate concern.
- Replacing native `confirm()` outside `/alerts` — project-wide pattern; changing it all requires separate decision.
## Migration risk
- **Low.** Changes are localized to `ui/src/pages/Alerts/` plus one CSS module file for the wizard. No backend, no DS, no router changes.
- Openapi schema regeneration **not required** (no controller changes).
- Existing tests don't exercise feed-row DOM directly (component tests cover badges/chips only), so row-to-table conversion won't break assertions.