feat(alerting): Plan 03 — UI + backfills (SSRF guard, metrics caching, docker stack) #144
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user