From e0496fdba2bd91608df88eed3946d5a663bfc220 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:58:17 +0200 Subject: [PATCH] =?UTF-8?q?ui(alerts):=20ReviewStep=20=E2=80=94=20render-p?= =?UTF-8?q?review=20pane=20for=20existing=20rules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire up the existing POST /alerts/rules/{id}/render-preview endpoint so rule authors can preview their Mustache-templated notification before saving. Available in edit mode only (new rules require save first — endpoint is id-bound). Matches the njams gap: their rules builder ships no in-builder preview and operators compensate with trial-and-error save/retry. Implementation notes: - ReviewStep gains an optional `ruleId` prop; when present, a "Preview notification" button calls `useRenderPreview` (the existing TanStack mutation in api/queries/alertRules.ts) and renders title + message in a titled, read-only pane styled like a notification card. - Errors surface as a DS Alert (variant=error) beneath the button. - `RuleEditorWizard` passes `ruleId={id}` through — mirrors the existing TriggerStep / NotifyStep wiring. - No stateless (/render-preview without id) variant exists on the backend, so for new rules the button is simply omitted. Co-Authored-By: Claude Opus 4.7 (1M context) --- ui/src/pages/Alerts/RuleEditor/ReviewStep.tsx | 81 ++++++++++++++++++- .../Alerts/RuleEditor/RuleEditorWizard.tsx | 2 +- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/ui/src/pages/Alerts/RuleEditor/ReviewStep.tsx b/ui/src/pages/Alerts/RuleEditor/ReviewStep.tsx index d2febd96..6330de33 100644 --- a/ui/src/pages/Alerts/RuleEditor/ReviewStep.tsx +++ b/ui/src/pages/Alerts/RuleEditor/ReviewStep.tsx @@ -1,5 +1,7 @@ -import { useMemo } from 'react'; -import { Alert, Toggle } from '@cameleer/design-system'; +import { useMemo, useState } from 'react'; +import { Alert, Button, Toggle } from '@cameleer/design-system'; +import { useRenderPreview } from '../../../api/queries/alertRules'; +import { describeApiError } from '../../../api/errors'; import { toRequest, type FormState } from './form-state'; /** @@ -24,12 +26,36 @@ export function computeSaveBlockReason(form: FormState): string | null { export function ReviewStep({ form, setForm, + ruleId, }: { form: FormState; setForm?: (f: FormState) => void; + /** + * Present only in edit mode. When absent the notification-preview button is + * hidden, because the backend `/render-preview` endpoint is id-bound and + * has no stateless variant — rendering against an unsaved draft would + * require a new endpoint and is explicitly out of scope here. + */ + ruleId?: string; }) { const req = toRequest(form); const saveBlockReason = useMemo(() => computeSaveBlockReason(form), [form]); + + const previewMutation = useRenderPreview(); + const [preview, setPreview] = useState<{ title: string; message: string } | null>(null); + const [previewError, setPreviewError] = useState(null); + + const onPreview = async () => { + setPreviewError(null); + try { + const res = await previewMutation.mutateAsync({ id: ruleId!, req: {} }); + setPreview({ title: res.title ?? '', message: res.message ?? '' }); + } catch (e) { + setPreview(null); + setPreviewError(describeApiError(e)); + } + }; + return (
{saveBlockReason && ( @@ -69,6 +95,57 @@ export function ReviewStep({ />
)} + + {ruleId && ( +
+
+ +
+ {previewError && ( + + {previewError} + + )} + {preview && ( +
+
+ Rendered notification preview +
+
+ {preview.title || (empty title)} +
+
+                {preview.message || '(empty message)'}
+              
+
+ )} +
+ )} +
Raw request JSON
     ) : (
-      
+      
     );
 
   return (