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:
@@ -20,7 +20,10 @@ import { ConditionStep } from './ConditionStep';
|
|||||||
import { TriggerStep } from './TriggerStep';
|
import { TriggerStep } from './TriggerStep';
|
||||||
import { NotifyStep } from './NotifyStep';
|
import { NotifyStep } from './NotifyStep';
|
||||||
import { ReviewStep } from './ReviewStep';
|
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';
|
import css from './wizard.module.css';
|
||||||
|
|
||||||
const STEP_LABELS: Record<WizardStep, string> = {
|
const STEP_LABELS: Record<WizardStep, string> = {
|
||||||
@@ -47,8 +50,20 @@ export default function RuleEditorWizard() {
|
|||||||
const promoteRuleId = search.get('ruleId') ?? undefined;
|
const promoteRuleId = search.get('ruleId') ?? undefined;
|
||||||
const sourceRuleQuery = useAlertRule(promoteFrom ? promoteRuleId : 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 [step, setStep] = useState<WizardStep>('scope');
|
||||||
const [form, setForm] = useState<FormState | null>(null);
|
const [form, setForm] = useState<FormState | null>(null);
|
||||||
|
const [warnings, setWarnings] = useState<PrefillWarning[]>([]);
|
||||||
|
|
||||||
// Initialize form once the existing or source rule loads.
|
// Initialize form once the existing or source rule loads.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -58,14 +73,29 @@ export default function RuleEditorWizard() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (promoteFrom && sourceRuleQuery.data) {
|
if (promoteFrom && sourceRuleQuery.data) {
|
||||||
const { form: prefilled } = prefillFromPromotion(sourceRuleQuery.data);
|
const { form: prefilled, warnings: w } = prefillFromPromotion(sourceRuleQuery.data, {
|
||||||
|
targetEnvAppSlugs: targetAppSlugs,
|
||||||
|
targetEnvAllowedConnectionIds: targetAllowedConnIds,
|
||||||
|
});
|
||||||
setForm(prefilled);
|
setForm(prefilled);
|
||||||
|
setWarnings(w);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isEdit && !promoteFrom) {
|
if (!isEdit && !promoteFrom) {
|
||||||
setForm(initialForm());
|
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 create = useCreateAlertRule();
|
||||||
const update = useUpdateAlertRule(id ?? '');
|
const update = useUpdateAlertRule(id ?? '');
|
||||||
@@ -124,6 +154,18 @@ export default function RuleEditorWizard() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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}>
|
<nav className={css.steps}>
|
||||||
{WIZARD_STEPS.map((s, i) => (
|
{WIZARD_STEPS.map((s, i) => (
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user