From f096365e0509d66db5e547d891e01d5d29830a6c Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:55:13 +0200 Subject: [PATCH] ui(alerts): ReviewStep blocks save on empty webhooks+targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shows a warning banner and disables the Save button when a rule has neither webhooks nor targets — would have been rejected at the server edge (Task 3.3 validator), now caught earlier in the wizard with clear reason. --- ui/src/pages/Alerts/RuleEditor/ReviewStep.tsx | 28 ++++++++++++++++++- .../Alerts/RuleEditor/RuleEditorWizard.tsx | 8 ++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/ui/src/pages/Alerts/RuleEditor/ReviewStep.tsx b/ui/src/pages/Alerts/RuleEditor/ReviewStep.tsx index bda16fd5..d2febd96 100644 --- a/ui/src/pages/Alerts/RuleEditor/ReviewStep.tsx +++ b/ui/src/pages/Alerts/RuleEditor/ReviewStep.tsx @@ -1,6 +1,26 @@ -import { Toggle } from '@cameleer/design-system'; +import { useMemo } from 'react'; +import { Alert, Toggle } from '@cameleer/design-system'; import { toRequest, type FormState } from './form-state'; +/** + * Pure helper: returns a human-readable reason why saving should be blocked, + * or null when the rule is safe to save (from the wizard's perspective). + * + * Currently covers: a rule with no webhooks AND no targets would be rejected + * at the server edge (Task 3.3 validator) and would never notify anyone, so + * the wizard blocks it earlier with a clear reason. The helper is exported + * so `RuleEditorWizard` can also drive the Save button's `disabled` state + * off the same single source of truth. + */ +export function computeSaveBlockReason(form: FormState): string | null { + const noWebhooks = (form.webhooks ?? []).length === 0; + const noTargets = (form.targets ?? []).length === 0; + if (noWebhooks && noTargets) { + return 'Add at least one webhook or target \u2014 a rule with no recipients never notifies anyone.'; + } + return null; +} + export function ReviewStep({ form, setForm, @@ -9,8 +29,14 @@ export function ReviewStep({ setForm?: (f: FormState) => void; }) { const req = toRequest(form); + const saveBlockReason = useMemo(() => computeSaveBlockReason(form), [form]); return (
+ {saveBlockReason && ( + + {saveBlockReason} + + )}
Name: {form.name}
diff --git a/ui/src/pages/Alerts/RuleEditor/RuleEditorWizard.tsx b/ui/src/pages/Alerts/RuleEditor/RuleEditorWizard.tsx index 62c502cf..d70167cb 100644 --- a/ui/src/pages/Alerts/RuleEditor/RuleEditorWizard.tsx +++ b/ui/src/pages/Alerts/RuleEditor/RuleEditorWizard.tsx @@ -19,7 +19,7 @@ import { ScopeStep } from './ScopeStep'; import { ConditionStep } from './ConditionStep'; import { TriggerStep } from './TriggerStep'; import { NotifyStep } from './NotifyStep'; -import { ReviewStep } from './ReviewStep'; +import { ReviewStep, computeSaveBlockReason } from './ReviewStep'; import { prefillFromPromotion, type PrefillWarning } from './promotion-prefill'; import { useCatalog } from '../../../api/queries/catalog'; import { useOutboundConnections } from '../../../api/queries/admin/outboundConnections'; @@ -194,7 +194,11 @@ export default function RuleEditorWizard() { Next ) : ( - )}