diff --git a/ui/src/pages/Alerts/RuleEditor/NotifyStep.tsx b/ui/src/pages/Alerts/RuleEditor/NotifyStep.tsx
index 59d4d853..d8272ce7 100644
--- a/ui/src/pages/Alerts/RuleEditor/NotifyStep.tsx
+++ b/ui/src/pages/Alerts/RuleEditor/NotifyStep.tsx
@@ -1,13 +1,250 @@
+import { useState } from 'react';
+import { Badge, Button, FormField, Input, Select, useToast } from '@cameleer/design-system';
+import { MustacheEditor } from '../../../components/MustacheEditor/MustacheEditor';
+import { useUsers, useGroups, useRoles } from '../../../api/queries/admin/rbac';
+import { useOutboundConnections } from '../../../api/queries/admin/outboundConnections';
+import { useSelectedEnv } from '../../../api/queries/alertMeta';
+import { useRenderPreview } from '../../../api/queries/alertRules';
import type { FormState } from './form-state';
+type TargetKind = FormState['targets'][number]['kind'];
+
export function NotifyStep({
- form: _form,
- setForm: _setForm,
- ruleId: _ruleId,
+ form,
+ setForm,
+ ruleId,
}: {
form: FormState;
setForm: (f: FormState) => void;
ruleId?: string;
}) {
- return
Notify step — TODO Task 23
;
+ const env = useSelectedEnv();
+ const { data: users } = useUsers(true);
+ const { data: groups } = useGroups(true);
+ const { data: roles } = useRoles(true);
+ const { data: connections } = useOutboundConnections();
+ const preview = useRenderPreview();
+ const { toast } = useToast();
+ const [lastPreview, setLastPreview] = useState(null);
+
+ // Filter connections to those that allow the current env.
+ const availableConnections = (connections ?? []).filter(
+ (c) => c.allowedEnvironmentIds.length === 0 || (!!env && c.allowedEnvironmentIds.includes(env)),
+ );
+
+ const onPreview = async () => {
+ if (!ruleId) {
+ toast({ title: 'Save rule first to preview', variant: 'error' });
+ return;
+ }
+ try {
+ const res = await preview.mutateAsync({ id: ruleId, req: {} });
+ setLastPreview(`TITLE:\n${res.title ?? ''}\n\nMESSAGE:\n${res.message ?? ''}`);
+ } catch (e) {
+ toast({ title: 'Preview failed', description: String(e), variant: 'error' });
+ }
+ };
+
+ const addTarget = (kind: TargetKind, targetId: string) => {
+ if (!targetId) return;
+ if (form.targets.some((t) => t.kind === kind && t.targetId === targetId)) return;
+ setForm({ ...form, targets: [...form.targets, { kind, targetId }] });
+ };
+ const removeTarget = (idx: number) => {
+ setForm({ ...form, targets: form.targets.filter((_, i) => i !== idx) });
+ };
+
+ const addWebhook = (outboundConnectionId: string) => {
+ setForm({
+ ...form,
+ webhooks: [...form.webhooks, { outboundConnectionId, bodyOverride: '', headerOverrides: [] }],
+ });
+ };
+ const removeWebhook = (idx: number) => {
+ setForm({ ...form, webhooks: form.webhooks.filter((_, i) => i !== idx) });
+ };
+ const updateWebhook = (idx: number, patch: Partial) => {
+ setForm({
+ ...form,
+ webhooks: form.webhooks.map((w, i) => (i === idx ? { ...w, ...patch } : w)),
+ });
+ };
+
+ return (
+
+
setForm({ ...form, notificationTitleTmpl: v })}
+ kind={form.conditionKind}
+ singleLine
+ />
+ setForm({ ...form, notificationMessageTmpl: v })}
+ kind={form.conditionKind}
+ minHeight={120}
+ />
+
+
+ {!ruleId && (
+
+ Save the rule first to preview rendered output.
+
+ )}
+ {lastPreview && (
+
+ {lastPreview}
+
+ )}
+
+
+
+
+ {form.targets.map((t, i) => (
+ removeTarget(i)}
+ />
+ ))}
+
+
+
+
+
+
+ {
+ if (e.target.value) addWebhook(e.target.value);
+ e.target.value = '';
+ }}
+ options={[
+ { value: '', label: '+ Add webhook' },
+ ...availableConnections.map((c) => ({ value: c.id, label: c.name })),
+ ]}
+ />
+ {form.webhooks.map((w, i) => {
+ const conn = availableConnections.find((c) => c.id === w.outboundConnectionId);
+ return (
+
+
+ {conn?.name ?? w.outboundConnectionId}
+
+
+
updateWebhook(i, { bodyOverride: v })}
+ kind={form.conditionKind}
+ placeholder="Leave empty to use connection default"
+ minHeight={80}
+ />
+
+ {w.headerOverrides.map((h, hi) => (
+
+ {
+ const heads = [...w.headerOverrides];
+ heads[hi] = { ...heads[hi], key: e.target.value };
+ updateWebhook(i, { headerOverrides: heads });
+ }}
+ />
+ {
+ const heads = [...w.headerOverrides];
+ heads[hi] = { ...heads[hi], value: e.target.value };
+ updateWebhook(i, { headerOverrides: heads });
+ }}
+ />
+
+
+ ))}
+
+
+
+ );
+ })}
+
+
+ );
}