Files
cameleer-server/ui/src/pages/Alerts/SilencesPage.tsx

172 lines
5.6 KiB
TypeScript
Raw Normal View History

import { useState } from 'react';
import { BellOff } from 'lucide-react';
import {
fix(alerts/ui): page header, scroll, title preview, bell badge polish Visual regressions surfaced during browser smoke: 1. Page headers — `SectionHeader` renders as 12px uppercase gray (a section divider, not a page title). Replace with proper h2 title + inline subtitle (`N firing · N total` etc.) and right-aligned actions, styled from `alerts-page.module.css`. 2. Undefined `--space-*` tokens — the project (and `@cameleer/design-system`) has never shipped `--space-sm|md|lg|xl`, even though many modules (SensitiveKeysPage, alerts CSS, …) reference them. The fallback to `initial` silently collapsed gaps/paddings to 0. Define the scale in `ui/src/index.css` so every consumer picks it up. 3. List scrolling — DataTable was using default pagination, but with no flex sizing the whole page scrolled. Add `fillHeight` and raise `pageSize`/list `limit` to 200 so the table gets sticky header + internal scroll + pinned pagination footer (Gmail-style). True cursor-based infinite scroll needs a backend change (filed as follow-up — `/alerts` only accepts `limit` today). 4. Title column clipping — `.titlePreview` used `white-space: nowrap` + fixed `max-width`, truncating message mid-UUID. Switch to a 2-line `-webkit-line-clamp` so full context is visible. 5. Notification bell badge invisible — `NotificationBell.module.css` referenced undefined tokens (`--fg`, `--hover-bg`, `--bg`, `--muted`). Map to real DS tokens (`--text-primary`, `--bg-hover`, `#fff`, `--text-muted`). The admin user currently sees no badge because the backend `/alerts/unread-count` returns 0 (read receipts) — that's data, not UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 10:40:28 +02:00
Button, FormField, Input, useToast, DataTable,
EmptyState, ConfirmDialog, MonoText,
} from '@cameleer/design-system';
import type { Column } from '@cameleer/design-system';
import { PageLoader } from '../../components/PageLoader';
import {
useAlertSilences,
useCreateSilence,
useDeleteSilence,
type AlertSilenceResponse,
} from '../../api/queries/alertSilences';
import sectionStyles from '../../styles/section-card.module.css';
import tableStyles from '../../styles/table-section.module.css';
import css from './alerts-page.module.css';
export default function SilencesPage() {
const { data, isLoading, error } = useAlertSilences();
const create = useCreateSilence();
const remove = useDeleteSilence();
const { toast } = useToast();
const [reason, setReason] = useState('');
const [matcherRuleId, setMatcherRuleId] = useState('');
const [matcherAppSlug, setMatcherAppSlug] = useState('');
const [hours, setHours] = useState(1);
const [pendingEnd, setPendingEnd] = useState<AlertSilenceResponse | null>(null);
if (isLoading) return <PageLoader />;
if (error) return <div className={css.page}>Failed to load silences: {String(error)}</div>;
const rows = data ?? [];
const onCreate = async () => {
const now = new Date();
const endsAt = new Date(now.getTime() + hours * 3600_000);
const matcher: Record<string, string> = {};
if (matcherRuleId) matcher.ruleId = matcherRuleId;
if (matcherAppSlug) matcher.appSlug = matcherAppSlug;
if (Object.keys(matcher).length === 0) {
toast({ title: 'Silence needs at least one matcher field', variant: 'error' });
return;
}
try {
await create.mutateAsync({
matcher,
reason: reason || undefined,
startsAt: now.toISOString(),
endsAt: endsAt.toISOString(),
});
setReason('');
setMatcherRuleId('');
setMatcherAppSlug('');
setHours(1);
toast({ title: 'Silence created', variant: 'success' });
} catch (e) {
toast({ title: 'Create failed', description: String(e), variant: 'error' });
}
};
const confirmEnd = async () => {
if (!pendingEnd) return;
try {
await remove.mutateAsync(pendingEnd.id!);
toast({ title: 'Silence removed', variant: 'success' });
} catch (e) {
toast({ title: 'Remove failed', description: String(e), variant: 'error' });
} finally {
setPendingEnd(null);
}
};
const columns: Column<AlertSilenceResponse & { id: string }>[] = [
{
key: 'matcher', header: 'Matcher',
render: (_, s) => <MonoText size="xs">{JSON.stringify(s.matcher)}</MonoText>,
},
{ key: 'reason', header: 'Reason', render: (_, s) => s.reason ?? '—' },
{ key: 'startsAt', header: 'Starts', width: '200px' },
{ key: 'endsAt', header: 'Ends', width: '200px' },
{
key: 'actions', header: '', width: '90px',
render: (_, s) => (
<Button variant="ghost" size="sm" onClick={() => setPendingEnd(s)}>
fix(alerts/ui): bell position, content tabs hidden, filters, novice labels Surfaced during second smoke: 1. Notification bell moved — was first child of TopBar (left of breadcrumb); now rendered inside the `environment` slot so it sits between the env selector and the user menu, matching user expectations. 2. Content tabs (Exchanges/Dashboard/Runtime/Deployments) hidden on `/alerts/*` — the operational tabs don't apply there. 3. Inbox / All alerts filters now actually filter. `AlertController.list` accepts only `limit` — `state`/`severity` query params are dropped server-side. Move `useAlerts` to fetch once per env (limit 200) and apply filters client-side via react-query `select`, with a stable queryKey so filter toggles are instant and don't re-request. True server-side filter needs a backend change (follow-up). 4. Novice-friendly labels: - Inbox subtitle: "99 firing · 100 total" → "99 need attention · 100 total in inbox" - All alerts filter: Open/Firing/Acked/All → "Currently open"/"Firing now"/"Acknowledged"/"All states" - All alerts subtitle: "N shown" → "N matching your filter" - History subtitle: "N resolved" → "N resolved alert(s) in range" - Rules subtitle: "N total" → "N rule(s) configured" - Silences subtitle: "N active" → "N active silence(s)" or "Nothing silenced right now" - Column headers: "State" → "Status", rules "Kind" → "Type", rules "Targets" → "Notifies" - Buttons: "Ack" → "Acknowledge", silence "End" → "End early" Updated alerts.test.tsx and e2e selector to match new behavior/labels. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 11:48:33 +02:00
End early
</Button>
),
},
];
return (
<div className={css.page}>
fix(alerts/ui): page header, scroll, title preview, bell badge polish Visual regressions surfaced during browser smoke: 1. Page headers — `SectionHeader` renders as 12px uppercase gray (a section divider, not a page title). Replace with proper h2 title + inline subtitle (`N firing · N total` etc.) and right-aligned actions, styled from `alerts-page.module.css`. 2. Undefined `--space-*` tokens — the project (and `@cameleer/design-system`) has never shipped `--space-sm|md|lg|xl`, even though many modules (SensitiveKeysPage, alerts CSS, …) reference them. The fallback to `initial` silently collapsed gaps/paddings to 0. Define the scale in `ui/src/index.css` so every consumer picks it up. 3. List scrolling — DataTable was using default pagination, but with no flex sizing the whole page scrolled. Add `fillHeight` and raise `pageSize`/list `limit` to 200 so the table gets sticky header + internal scroll + pinned pagination footer (Gmail-style). True cursor-based infinite scroll needs a backend change (filed as follow-up — `/alerts` only accepts `limit` today). 4. Title column clipping — `.titlePreview` used `white-space: nowrap` + fixed `max-width`, truncating message mid-UUID. Switch to a 2-line `-webkit-line-clamp` so full context is visible. 5. Notification bell badge invisible — `NotificationBell.module.css` referenced undefined tokens (`--fg`, `--hover-bg`, `--bg`, `--muted`). Map to real DS tokens (`--text-primary`, `--bg-hover`, `#fff`, `--text-muted`). The admin user currently sees no badge because the backend `/alerts/unread-count` returns 0 (read receipts) — that's data, not UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 10:40:28 +02:00
<header className={css.pageHeader}>
<div className={css.pageTitleGroup}>
<h2 className={css.pageTitle}>Alert silences</h2>
fix(alerts/ui): bell position, content tabs hidden, filters, novice labels Surfaced during second smoke: 1. Notification bell moved — was first child of TopBar (left of breadcrumb); now rendered inside the `environment` slot so it sits between the env selector and the user menu, matching user expectations. 2. Content tabs (Exchanges/Dashboard/Runtime/Deployments) hidden on `/alerts/*` — the operational tabs don't apply there. 3. Inbox / All alerts filters now actually filter. `AlertController.list` accepts only `limit` — `state`/`severity` query params are dropped server-side. Move `useAlerts` to fetch once per env (limit 200) and apply filters client-side via react-query `select`, with a stable queryKey so filter toggles are instant and don't re-request. True server-side filter needs a backend change (follow-up). 4. Novice-friendly labels: - Inbox subtitle: "99 firing · 100 total" → "99 need attention · 100 total in inbox" - All alerts filter: Open/Firing/Acked/All → "Currently open"/"Firing now"/"Acknowledged"/"All states" - All alerts subtitle: "N shown" → "N matching your filter" - History subtitle: "N resolved" → "N resolved alert(s) in range" - Rules subtitle: "N total" → "N rule(s) configured" - Silences subtitle: "N active" → "N active silence(s)" or "Nothing silenced right now" - Column headers: "State" → "Status", rules "Kind" → "Type", rules "Targets" → "Notifies" - Buttons: "Ack" → "Acknowledge", silence "End" → "End early" Updated alerts.test.tsx and e2e selector to match new behavior/labels. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 11:48:33 +02:00
<span className={css.pageSubtitle}>
{rows.length === 0
? 'Nothing silenced right now'
: `${rows.length} active silence${rows.length === 1 ? '' : 's'}`}
</span>
fix(alerts/ui): page header, scroll, title preview, bell badge polish Visual regressions surfaced during browser smoke: 1. Page headers — `SectionHeader` renders as 12px uppercase gray (a section divider, not a page title). Replace with proper h2 title + inline subtitle (`N firing · N total` etc.) and right-aligned actions, styled from `alerts-page.module.css`. 2. Undefined `--space-*` tokens — the project (and `@cameleer/design-system`) has never shipped `--space-sm|md|lg|xl`, even though many modules (SensitiveKeysPage, alerts CSS, …) reference them. The fallback to `initial` silently collapsed gaps/paddings to 0. Define the scale in `ui/src/index.css` so every consumer picks it up. 3. List scrolling — DataTable was using default pagination, but with no flex sizing the whole page scrolled. Add `fillHeight` and raise `pageSize`/list `limit` to 200 so the table gets sticky header + internal scroll + pinned pagination footer (Gmail-style). True cursor-based infinite scroll needs a backend change (filed as follow-up — `/alerts` only accepts `limit` today). 4. Title column clipping — `.titlePreview` used `white-space: nowrap` + fixed `max-width`, truncating message mid-UUID. Switch to a 2-line `-webkit-line-clamp` so full context is visible. 5. Notification bell badge invisible — `NotificationBell.module.css` referenced undefined tokens (`--fg`, `--hover-bg`, `--bg`, `--muted`). Map to real DS tokens (`--text-primary`, `--bg-hover`, `#fff`, `--text-muted`). The admin user currently sees no badge because the backend `/alerts/unread-count` returns 0 (read receipts) — that's data, not UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 10:40:28 +02:00
</div>
</header>
<section className={sectionStyles.section}>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, minmax(0, 1fr)) auto',
gap: 'var(--space-sm)',
alignItems: 'end',
}}
>
<FormField label="Rule ID" hint="Exact rule id (optional)">
<Input value={matcherRuleId} onChange={(e) => setMatcherRuleId(e.target.value)} />
</FormField>
<FormField label="App slug" hint="App slug (optional)">
<Input value={matcherAppSlug} onChange={(e) => setMatcherAppSlug(e.target.value)} />
</FormField>
<FormField label="Duration" hint="Hours">
<Input
type="number"
min={1}
value={hours}
onChange={(e) => setHours(Number(e.target.value))}
/>
</FormField>
<FormField label="Reason" hint="Context for operators">
<Input
value={reason}
onChange={(e) => setReason(e.target.value)}
placeholder="Maintenance window"
/>
</FormField>
<Button variant="primary" size="sm" onClick={onCreate} disabled={create.isPending}>
Create silence
</Button>
</div>
</section>
{rows.length === 0 ? (
<EmptyState
icon={<BellOff size={32} />}
title="No silences"
description="Nothing is currently silenced in this environment."
/>
) : (
fix(alerts/ui): page header, scroll, title preview, bell badge polish Visual regressions surfaced during browser smoke: 1. Page headers — `SectionHeader` renders as 12px uppercase gray (a section divider, not a page title). Replace with proper h2 title + inline subtitle (`N firing · N total` etc.) and right-aligned actions, styled from `alerts-page.module.css`. 2. Undefined `--space-*` tokens — the project (and `@cameleer/design-system`) has never shipped `--space-sm|md|lg|xl`, even though many modules (SensitiveKeysPage, alerts CSS, …) reference them. The fallback to `initial` silently collapsed gaps/paddings to 0. Define the scale in `ui/src/index.css` so every consumer picks it up. 3. List scrolling — DataTable was using default pagination, but with no flex sizing the whole page scrolled. Add `fillHeight` and raise `pageSize`/list `limit` to 200 so the table gets sticky header + internal scroll + pinned pagination footer (Gmail-style). True cursor-based infinite scroll needs a backend change (filed as follow-up — `/alerts` only accepts `limit` today). 4. Title column clipping — `.titlePreview` used `white-space: nowrap` + fixed `max-width`, truncating message mid-UUID. Switch to a 2-line `-webkit-line-clamp` so full context is visible. 5. Notification bell badge invisible — `NotificationBell.module.css` referenced undefined tokens (`--fg`, `--hover-bg`, `--bg`, `--muted`). Map to real DS tokens (`--text-primary`, `--bg-hover`, `#fff`, `--text-muted`). The admin user currently sees no badge because the backend `/alerts/unread-count` returns 0 (read receipts) — that's data, not UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 10:40:28 +02:00
<div className={`${tableStyles.tableSection} ${css.tableWrap}`}>
<DataTable<AlertSilenceResponse & { id: string }>
columns={columns}
data={rows.map((s) => ({ ...s, id: s.id ?? '' }))}
flush
fix(alerts/ui): page header, scroll, title preview, bell badge polish Visual regressions surfaced during browser smoke: 1. Page headers — `SectionHeader` renders as 12px uppercase gray (a section divider, not a page title). Replace with proper h2 title + inline subtitle (`N firing · N total` etc.) and right-aligned actions, styled from `alerts-page.module.css`. 2. Undefined `--space-*` tokens — the project (and `@cameleer/design-system`) has never shipped `--space-sm|md|lg|xl`, even though many modules (SensitiveKeysPage, alerts CSS, …) reference them. The fallback to `initial` silently collapsed gaps/paddings to 0. Define the scale in `ui/src/index.css` so every consumer picks it up. 3. List scrolling — DataTable was using default pagination, but with no flex sizing the whole page scrolled. Add `fillHeight` and raise `pageSize`/list `limit` to 200 so the table gets sticky header + internal scroll + pinned pagination footer (Gmail-style). True cursor-based infinite scroll needs a backend change (filed as follow-up — `/alerts` only accepts `limit` today). 4. Title column clipping — `.titlePreview` used `white-space: nowrap` + fixed `max-width`, truncating message mid-UUID. Switch to a 2-line `-webkit-line-clamp` so full context is visible. 5. Notification bell badge invisible — `NotificationBell.module.css` referenced undefined tokens (`--fg`, `--hover-bg`, `--bg`, `--muted`). Map to real DS tokens (`--text-primary`, `--bg-hover`, `#fff`, `--text-muted`). The admin user currently sees no badge because the backend `/alerts/unread-count` returns 0 (read receipts) — that's data, not UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 10:40:28 +02:00
fillHeight
/>
</div>
)}
<ConfirmDialog
open={!!pendingEnd}
onClose={() => setPendingEnd(null)}
onConfirm={confirmEnd}
title="End silence?"
message="End this silence early? Affected rules will resume firing."
confirmText="End silence"
variant="warning"
loading={remove.isPending}
/>
</div>
);
}