diff --git a/ui/src/pages/Alerts/SilencesPage.tsx b/ui/src/pages/Alerts/SilencesPage.tsx index 2703b34b..77596eed 100644 --- a/ui/src/pages/Alerts/SilencesPage.tsx +++ b/ui/src/pages/Alerts/SilencesPage.tsx @@ -1,5 +1,10 @@ import { useState } from 'react'; -import { Button, FormField, Input, SectionHeader, useToast } from '@cameleer/design-system'; +import { BellOff } from 'lucide-react'; +import { + Button, FormField, Input, SectionHeader, useToast, DataTable, + EmptyState, ConfirmDialog, MonoText, +} from '@cameleer/design-system'; +import type { Column } from '@cameleer/design-system'; import { PageLoader } from '../../components/PageLoader'; import { useAlertSilences, @@ -8,6 +13,8 @@ import { 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(); @@ -19,9 +26,12 @@ export default function SilencesPage() { const [matcherRuleId, setMatcherRuleId] = useState(''); const [matcherAppSlug, setMatcherAppSlug] = useState(''); const [hours, setHours] = useState(1); + const [pendingEnd, setPendingEnd] = useState(null); if (isLoading) return ; - if (error) return
Failed to load silences: {String(error)}
; + if (error) return
Failed to load silences: {String(error)}
; + + const rows = data ?? []; const onCreate = async () => { const now = new Date(); @@ -50,30 +60,58 @@ export default function SilencesPage() { } }; - const onRemove = async (s: AlertSilenceResponse) => { - if (!confirm(`End silence early?`)) return; + const confirmEnd = async () => { + if (!pendingEnd) return; try { - await remove.mutateAsync(s.id!); + 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 rows = data ?? []; + const columns: Column[] = [ + { + key: 'matcher', header: 'Matcher', + render: (_, s) => {JSON.stringify(s.matcher)}, + }, + { 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) => ( + + ), + }, + ]; return ( -
- Alert silences -
-
- +
+
+ Alert silences +
+ +
+
+ setMatcherRuleId(e.target.value)} /> - + setMatcherAppSlug(e.target.value)} /> - + setHours(Number(e.target.value))} /> - + setReason(e.target.value)} @@ -92,39 +130,34 @@ export default function SilencesPage() { Create silence
-
-
- {rows.length === 0 ? ( -

No active or scheduled silences.

- ) : ( - - - - - - - - - - - - {rows.map((s) => ( - - - - - - - - ))} - -
MatcherReasonStartsEnds
{JSON.stringify(s.matcher)}{s.reason ?? '—'}{s.startsAt}{s.endsAt} - -
- )} -
+ + + {rows.length === 0 ? ( + } + title="No silences" + description="Nothing is currently silenced in this environment." + /> + ) : ( +
+ + columns={columns} + data={rows.map((s) => ({ ...s, id: s.id ?? '' }))} + flush + /> +
+ )} + + 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} + />
); }