feat(ui/alerts): render promotion warnings in wizard banner

Fetches target-env apps (useCatalog) and env-allowed outbound
connections, passes them to prefillFromPromotion, and renders the
returned warnings in an amber banner above the step nav. Warnings list
the field name and the remediation message so users see crossings that
need manual adjustment before saving.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-20 14:05:08 +02:00
parent 3963ea5591
commit 0191ca4b13

View File

@@ -20,7 +20,10 @@ import { ConditionStep } from './ConditionStep';
import { TriggerStep } from './TriggerStep';
import { NotifyStep } from './NotifyStep';
import { ReviewStep } from './ReviewStep';
import { prefillFromPromotion } from './promotion-prefill';
import { prefillFromPromotion, type PrefillWarning } from './promotion-prefill';
import { useCatalog } from '../../../api/queries/catalog';
import { useOutboundConnections } from '../../../api/queries/admin/outboundConnections';
import { useSelectedEnv } from '../../../api/queries/alertMeta';
import css from './wizard.module.css';
const STEP_LABELS: Record<WizardStep, string> = {
@@ -47,8 +50,20 @@ export default function RuleEditorWizard() {
const promoteRuleId = search.get('ruleId') ?? undefined;
const sourceRuleQuery = useAlertRule(promoteFrom ? promoteRuleId : undefined);
// Target-env data for promotion warnings.
const env = useSelectedEnv();
const targetEnv = search.get('targetEnv') ?? env;
const { data: targetCatalog } = useCatalog(targetEnv ?? undefined);
const { data: connections } = useOutboundConnections();
const targetAppSlugs = (targetCatalog ?? []).map((a) => a.slug);
const targetAllowedConnIds = (connections ?? [])
.filter((c) => c.allowedEnvironmentIds.length === 0 || (!!targetEnv && c.allowedEnvironmentIds.includes(targetEnv)))
.map((c) => c.id);
const [step, setStep] = useState<WizardStep>('scope');
const [form, setForm] = useState<FormState | null>(null);
const [warnings, setWarnings] = useState<PrefillWarning[]>([]);
// Initialize form once the existing or source rule loads.
useEffect(() => {
@@ -58,14 +73,29 @@ export default function RuleEditorWizard() {
return;
}
if (promoteFrom && sourceRuleQuery.data) {
const { form: prefilled } = prefillFromPromotion(sourceRuleQuery.data);
const { form: prefilled, warnings: w } = prefillFromPromotion(sourceRuleQuery.data, {
targetEnvAppSlugs: targetAppSlugs,
targetEnvAllowedConnectionIds: targetAllowedConnIds,
});
setForm(prefilled);
setWarnings(w);
return;
}
if (!isEdit && !promoteFrom) {
setForm(initialForm());
}
}, [form, isEdit, existingQuery.data, promoteFrom, sourceRuleQuery.data]);
// Intentionally depend on join()'d slug/id strings so the effect
// doesn't retrigger on new array identities when contents are equal.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
form,
isEdit,
existingQuery.data,
promoteFrom,
sourceRuleQuery.data,
targetAppSlugs.join(','),
targetAllowedConnIds.join(','),
]);
const create = useCreateAlertRule();
const update = useUpdateAlertRule(id ?? '');
@@ -124,6 +154,18 @@ export default function RuleEditorWizard() {
</div>
)}
</div>
{warnings.length > 0 && (
<div className={css.promoteBanner}>
<strong>Review before saving:</strong>
<ul style={{ margin: '4px 0 0 16px', padding: 0 }}>
{warnings.map((w) => (
<li key={w.field}>
<code>{w.field}</code>: {w.message}
</li>
))}
</ul>
</div>
)}
<nav className={css.steps}>
{WIZARD_STEPS.map((s, i) => (
<button