diff --git a/ui/src/pages/Alerts/RulesListPage.tsx b/ui/src/pages/Alerts/RulesListPage.tsx index cdd2bd6a..c4489bce 100644 --- a/ui/src/pages/Alerts/RulesListPage.tsx +++ b/ui/src/pages/Alerts/RulesListPage.tsx @@ -1,5 +1,11 @@ +import { useState } from 'react'; import { Link, useNavigate } from 'react-router'; -import { Button, SectionHeader, Toggle, useToast, Badge } from '@cameleer/design-system'; +import { FilePlus } from 'lucide-react'; +import { + Button, SectionHeader, Toggle, useToast, Badge, DataTable, + EmptyState, Dropdown, ConfirmDialog, +} from '@cameleer/design-system'; +import type { Column } from '@cameleer/design-system'; import { PageLoader } from '../../components/PageLoader'; import { SeverityBadge } from '../../components/SeverityBadge'; import { @@ -10,7 +16,8 @@ import { } from '../../api/queries/alertRules'; import { useEnvironments } from '../../api/queries/admin/environments'; import { useSelectedEnv } from '../../api/queries/alertMeta'; -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 RulesListPage() { const navigate = useNavigate(); @@ -21,28 +28,32 @@ export default function RulesListPage() { const deleteRule = useDeleteAlertRule(); const { toast } = useToast(); + const [pendingDelete, setPendingDelete] = useState(null); + if (isLoading) return ; - if (error) return
Failed to load rules: {String(error)}
; + if (error) return
Failed to load rules: {String(error)}
; const rows = rules ?? []; const otherEnvs = (envs ?? []).filter((e) => e.slug !== env); const onToggle = async (r: AlertRuleResponse) => { try { - await setEnabled.mutateAsync({ id: r.id, enabled: !r.enabled }); + await setEnabled.mutateAsync({ id: r.id!, enabled: !r.enabled }); toast({ title: r.enabled ? 'Disabled' : 'Enabled', description: r.name, variant: 'success' }); } catch (e) { toast({ title: 'Toggle failed', description: String(e), variant: 'error' }); } }; - const onDelete = async (r: AlertRuleResponse) => { - if (!confirm(`Delete rule "${r.name}"? Fired alerts are preserved via rule_snapshot.`)) return; + const confirmDelete = async () => { + if (!pendingDelete) return; try { - await deleteRule.mutateAsync(r.id); - toast({ title: 'Deleted', description: r.name, variant: 'success' }); + await deleteRule.mutateAsync(pendingDelete.id!); + toast({ title: 'Deleted', description: pendingDelete.name, variant: 'success' }); } catch (e) { toast({ title: 'Delete failed', description: String(e), variant: 'error' }); + } finally { + setPendingDelete(null); } }; @@ -50,66 +61,103 @@ export default function RulesListPage() { navigate(`/alerts/rules/new?promoteFrom=${env}&ruleId=${r.id}&targetEnv=${targetEnvSlug}`); }; + const columns: Column[] = [ + { + key: 'name', header: 'Name', + render: (_, r) => {r.name}, + }, + { + key: 'conditionKind', header: 'Kind', width: '160px', + render: (_, r) => , + }, + { + key: 'severity', header: 'Severity', width: '110px', + render: (_, r) => , + }, + { + key: 'enabled', header: 'Enabled', width: '90px', + render: (_, r) => ( + onToggle(r)} + disabled={setEnabled.isPending} + /> + ), + }, + { + key: 'targets', header: 'Targets', width: '90px', + render: (_, r) => String(r.targets?.length ?? 0), + }, + { + key: 'actions', header: '', width: '220px', + render: (_, r) => ( +
+ {otherEnvs.length > 0 && ( + Promote to ▾} + items={otherEnvs.map((e) => ({ + label: e.slug, + onClick: () => onPromote(r, e.slug), + }))} + /> + )} + +
+ ), + }, + ]; + return ( -
-
- Alert rules - - - -
-
- {rows.length === 0 ? ( -

No rules yet. Create one to start evaluating alerts for this environment.

- ) : ( - - - - - - - - - - - - - {rows.map((r) => ( - - - - - - - - - ))} - -
NameKindSeverityEnabledTargets
{r.name} - onToggle(r)} - disabled={setEnabled.isPending} - /> - {r.targets.length} - {otherEnvs.length > 0 && ( - - )} - -
- )} +
+
+ + + + } + > + Alert rules +
+ + {rows.length === 0 ? ( + } + title="No alert rules" + description="Create one to start evaluating alerts for this environment." + action={ + + + + } + /> + ) : ( +
+ + columns={columns} + data={rows as (AlertRuleResponse & { id: string })[]} + flush + /> +
+ )} + + setPendingDelete(null)} + onConfirm={confirmDelete} + title="Delete alert rule?" + message={ + pendingDelete + ? `Delete rule "${pendingDelete.name}"? Fired alerts are preserved via rule_snapshot.` + : '' + } + confirmText="Delete" + variant="danger" + loading={deleteRule.isPending} + />
); }