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 {
Button, FormField, Input, useToast, DataTable,
EmptyState, ConfirmDialog, MonoText,
EmptyState, ConfirmDialog, MonoText, Modal,
} from '@cameleer/design-system';
import type { Column } from '@cameleer/design-system';
import { PageLoader } from '../../components/PageLoader';
@@ -14,7 +14,6 @@ import {
type AlertSilenceResponse,
} from '../../api/queries/alertSilences';
import { describeApiError } from '../../api/errors';
import sectionStyles from '../../styles/section-card.module.css';
import tableStyles from '../../styles/table-section.module.css';
import css from './alerts-page.module.css';
@@ -29,13 +28,29 @@ export default function SilencesPage() {
const [matcherAppSlug, setMatcherAppSlug] = useState('');
const [hours, setHours] = useState(1);
const [pendingEnd, setPendingEnd] = useState<AlertSilenceResponse | null>(null);
const [createOpen, setCreateOpen] = useState(false);
const [searchParams] = useSearchParams();
useEffect(() => {
const r = searchParams.get('ruleId');
if (r) setMatcherRuleId(r);
if (r) {
setMatcherRuleId(r);
setCreateOpen(true);
}
}, [searchParams]);
const resetForm = () => {
setReason('');
setMatcherRuleId('');
setMatcherAppSlug('');
setHours(1);
};
const closeModal = () => {
setCreateOpen(false);
resetForm();
};
if (isLoading) return <PageLoader />;
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(),
endsAt: endsAt.toISOString(),
});
setReason('');
setMatcherRuleId('');
setMatcherAppSlug('');
setHours(1);
closeModal();
toast({ title: 'Silence created', variant: 'success' });
} catch (e) {
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'}`}
</span>
</div>
<div className={css.pageActions}>
<Button variant="primary" onClick={() => setCreateOpen(true)}>New silence</Button>
</div>
</header>
<section className={sectionStyles.section}>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, minmax(0, 1fr)) auto',
gap: 'var(--space-sm)',
alignItems: 'end',
}}
>
{rows.length === 0 ? (
<EmptyState
icon={<BellOff size={32} />}
title="No silences"
description="Nothing is currently silenced in this environment."
action={
<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)">
<Input value={matcherRuleId} onChange={(e) => setMatcherRuleId(e.target.value)} />
</FormField>
@@ -141,28 +174,14 @@ export default function SilencesPage() {
placeholder="Maintenance window"
/>
</FormField>
<Button variant="primary" size="sm" onClick={onCreate} disabled={create.isPending}>
Create silence
</Button>
<div style={{ display: 'flex', gap: 'var(--space-sm)', justifyContent: 'flex-end' }}>
<Button variant="ghost" onClick={closeModal} disabled={create.isPending}>Cancel</Button>
<Button variant="primary" onClick={onCreate} disabled={create.isPending}>
Create silence
</Button>
</div>
</div>
</section>
{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>
)}
</Modal>
<ConfirmDialog
open={!!pendingEnd}