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() {
|
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