feat(ui/alerts): RulesListPage with enable/disable, delete, env promotion

Promotion dropdown builds a /alerts/rules/new URL with promoteFrom, ruleId,
and targetEnv query params — the wizard will read these in Task 24 and
pre-fill the form with source-env prefill + client-side warnings.
This commit is contained in:
hsiegeln
2026-04-20 13:52:14 +02:00
parent 269a63af1f
commit 7e91459cd6

View File

@@ -1,3 +1,115 @@
import { Link, useNavigate } from 'react-router';
import { Button, SectionHeader, Toggle, useToast, Badge } from '@cameleer/design-system';
import { PageLoader } from '../../components/PageLoader';
import { SeverityBadge } from '../../components/SeverityBadge';
import {
useAlertRules,
useDeleteAlertRule,
useSetAlertRuleEnabled,
type AlertRuleResponse,
} 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';
export default function RulesListPage() {
return <div>RulesListPage coming soon</div>;
const navigate = useNavigate();
const env = useSelectedEnv();
const { data: rules, isLoading, error } = useAlertRules();
const { data: envs } = useEnvironments();
const setEnabled = useSetAlertRuleEnabled();
const deleteRule = useDeleteAlertRule();
const { toast } = useToast();
if (isLoading) return <PageLoader />;
if (error) return <div>Failed to load rules: {String(error)}</div>;
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 });
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;
try {
await deleteRule.mutateAsync(r.id);
toast({ title: 'Deleted', description: r.name, variant: 'success' });
} catch (e) {
toast({ title: 'Delete failed', description: String(e), variant: 'error' });
}
};
const onPromote = (r: AlertRuleResponse, targetEnvSlug: string) => {
navigate(`/alerts/rules/new?promoteFrom=${env}&ruleId=${r.id}&targetEnv=${targetEnvSlug}`);
};
return (
<div style={{ padding: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<SectionHeader>Alert rules</SectionHeader>
<Link to="/alerts/rules/new">
<Button variant="primary">New rule</Button>
</Link>
</div>
<div className={sectionStyles.section}>
{rows.length === 0 ? (
<p>No rules yet. Create one to start evaluating alerts for this environment.</p>
) : (
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr>
<th style={{ textAlign: 'left' }}>Name</th>
<th style={{ textAlign: 'left' }}>Kind</th>
<th style={{ textAlign: 'left' }}>Severity</th>
<th style={{ textAlign: 'left' }}>Enabled</th>
<th style={{ textAlign: 'left' }}>Targets</th>
<th></th>
</tr>
</thead>
<tbody>
{rows.map((r) => (
<tr key={r.id}>
<td><Link to={`/alerts/rules/${r.id}`}>{r.name}</Link></td>
<td><Badge label={r.conditionKind} color="auto" variant="outlined" /></td>
<td><SeverityBadge severity={r.severity} /></td>
<td>
<Toggle
checked={r.enabled}
onChange={() => onToggle(r)}
disabled={setEnabled.isPending}
/>
</td>
<td>{r.targets.length}</td>
<td style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
{otherEnvs.length > 0 && (
<select
value=""
onChange={(e) => { if (e.target.value) onPromote(r, e.target.value); }}
aria-label={`Promote ${r.name} to another env`}
>
<option value="">Promote to </option>
{otherEnvs.map((e) => (
<option key={e.slug} value={e.slug}>{e.slug}</option>
))}
</select>
)}
<Button variant="secondary" onClick={() => onDelete(r)} disabled={deleteRule.isPending}>
Delete
</Button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
}