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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user