ui(alerts): Silences page adopts Rules UX — top-right button + modal form
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 2m28s
CI / docker (push) Has started running
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled

Before: the Silences page rendered an always-visible 4-field form strip
above the list, taking room even when the environment had zero silences.
Inconsistent with Rules, which puts a "New rule" action in the page
header and reserves the content area for either the list or an empty
state.

After: header mirrors Rules — title + subtitle on the left, a "New
silence" primary button on the right. The create form moved into a
Modal opened by that button (and by the empty-state's "Create silence"
action). `?ruleId=` deep links still work: the param is read on mount,
prefills the Rule ID field, and auto-opens the modal — preserving the
InboxPage "Silence rule… → Custom…" flow.

Dropped: unused `sectionStyles` import.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-22 09:09:13 +02:00
parent be45ba2d59
commit 1c4a98c0da

View File

@@ -3,7 +3,7 @@ import { useSearchParams } from 'react-router';
import { BellOff } from 'lucide-react'; import { BellOff } from 'lucide-react';
import { import {
Button, FormField, Input, useToast, DataTable, Button, FormField, Input, useToast, DataTable,
EmptyState, ConfirmDialog, MonoText, EmptyState, ConfirmDialog, MonoText, Modal,
} from '@cameleer/design-system'; } from '@cameleer/design-system';
import type { Column } from '@cameleer/design-system'; import type { Column } from '@cameleer/design-system';
import { PageLoader } from '../../components/PageLoader'; import { PageLoader } from '../../components/PageLoader';
@@ -14,7 +14,6 @@ import {
type AlertSilenceResponse, type AlertSilenceResponse,
} from '../../api/queries/alertSilences'; } from '../../api/queries/alertSilences';
import { describeApiError } from '../../api/errors'; import { describeApiError } from '../../api/errors';
import sectionStyles from '../../styles/section-card.module.css';
import tableStyles from '../../styles/table-section.module.css'; import tableStyles from '../../styles/table-section.module.css';
import css from './alerts-page.module.css'; import css from './alerts-page.module.css';
@@ -29,13 +28,29 @@ export default function SilencesPage() {
const [matcherAppSlug, setMatcherAppSlug] = useState(''); const [matcherAppSlug, setMatcherAppSlug] = useState('');
const [hours, setHours] = useState(1); const [hours, setHours] = useState(1);
const [pendingEnd, setPendingEnd] = useState<AlertSilenceResponse | null>(null); const [pendingEnd, setPendingEnd] = useState<AlertSilenceResponse | null>(null);
const [createOpen, setCreateOpen] = useState(false);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
useEffect(() => { useEffect(() => {
const r = searchParams.get('ruleId'); const r = searchParams.get('ruleId');
if (r) setMatcherRuleId(r); if (r) {
setMatcherRuleId(r);
setCreateOpen(true);
}
}, [searchParams]); }, [searchParams]);
const resetForm = () => {
setReason('');
setMatcherRuleId('');
setMatcherAppSlug('');
setHours(1);
};
const closeModal = () => {
setCreateOpen(false);
resetForm();
};
if (isLoading) return <PageLoader />; if (isLoading) return <PageLoader />;
if (error) return <div className={css.page}>Failed to load silences: {describeApiError(error)}</div>; if (error) return <div className={css.page}>Failed to load silences: {describeApiError(error)}</div>;
@@ -58,10 +73,7 @@ export default function SilencesPage() {
startsAt: now.toISOString(), startsAt: now.toISOString(),
endsAt: endsAt.toISOString(), endsAt: endsAt.toISOString(),
}); });
setReason(''); closeModal();
setMatcherRuleId('');
setMatcherAppSlug('');
setHours(1);
toast({ title: 'Silence created', variant: 'success' }); toast({ title: 'Silence created', variant: 'success' });
} catch (e) { } catch (e) {
toast({ title: 'Create failed', description: describeApiError(e), variant: 'error' }); toast({ title: 'Create failed', description: describeApiError(e), variant: 'error' });
@@ -109,17 +121,38 @@ export default function SilencesPage() {
: `${rows.length} active silence${rows.length === 1 ? '' : 's'}`} : `${rows.length} active silence${rows.length === 1 ? '' : 's'}`}
</span> </span>
</div> </div>
<div className={css.pageActions}>
<Button variant="primary" onClick={() => setCreateOpen(true)}>New silence</Button>
</div>
</header> </header>
<section className={sectionStyles.section}> {rows.length === 0 ? (
<div <EmptyState
style={{ icon={<BellOff size={32} />}
display: 'grid', title="No silences"
gridTemplateColumns: 'repeat(4, minmax(0, 1fr)) auto', description="Nothing is currently silenced in this environment."
gap: 'var(--space-sm)', action={
alignItems: 'end', <Button variant="primary" onClick={() => setCreateOpen(true)}>Create silence</Button>
}} }
> />
) : (
<div className={`${tableStyles.tableSection} ${css.tableWrap}`}>
<DataTable<AlertSilenceResponse & { id: string }>
columns={columns}
data={rows.map((s) => ({ ...s, id: s.id ?? '' }))}
flush
fillHeight
/>
</div>
)}
<Modal
open={createOpen}
onClose={closeModal}
title="New silence"
size="md"
>
<div style={{ display: 'grid', gap: 'var(--space-md)' }}>
<FormField label="Rule ID" hint="Exact rule id (optional)"> <FormField label="Rule ID" hint="Exact rule id (optional)">
<Input value={matcherRuleId} onChange={(e) => setMatcherRuleId(e.target.value)} /> <Input value={matcherRuleId} onChange={(e) => setMatcherRuleId(e.target.value)} />
</FormField> </FormField>
@@ -141,28 +174,14 @@ export default function SilencesPage() {
placeholder="Maintenance window" placeholder="Maintenance window"
/> />
</FormField> </FormField>
<Button variant="primary" size="sm" onClick={onCreate} disabled={create.isPending}> <div style={{ display: 'flex', gap: 'var(--space-sm)', justifyContent: 'flex-end' }}>
Create silence <Button variant="ghost" onClick={closeModal} disabled={create.isPending}>Cancel</Button>
</Button> <Button variant="primary" onClick={onCreate} disabled={create.isPending}>
Create silence
</Button>
</div>
</div> </div>
</section> </Modal>
{rows.length === 0 ? (
<EmptyState
icon={<BellOff size={32} />}
title="No silences"
description="Nothing is currently silenced in this environment."
/>
) : (
<div className={`${tableStyles.tableSection} ${css.tableWrap}`}>
<DataTable<AlertSilenceResponse & { id: string }>
columns={columns}
data={rows.map((s) => ({ ...s, id: s.id ?? '' }))}
flush
fillHeight
/>
</div>
)}
<ConfirmDialog <ConfirmDialog
open={!!pendingEnd} open={!!pendingEnd}