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 { 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}
|
||||||
|
|||||||
Reference in New Issue
Block a user