ui(alerts): Silences page adopts Rules UX — top-right button + modal form
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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user