feat(alerting): Plan 03 — UI + backfills (SSRF guard, metrics caching, docker stack) #144

Merged
hsiegeln merged 39 commits from feat/alerting-03-ui into main 2026-04-20 16:27:49 +02:00
Showing only changes of commit 0191ca4b13 - Show all commits

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