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)} + /> + ))} +
+
+ { + addTarget('GROUP', e.target.value); + e.target.value = ''; + }} + options={[ + { value: '', label: '+ Group' }, + ...(groups ?? []).map((g) => ({ value: g.id, label: g.name })), + ]} + /> + { + 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 }); + }} + /> + +
+ ))} + +
+
+ ); + })} + +
+ ); }