fix(onboarding): address final review findings — atomicity, scheduling guard, apply-all recovery, client validation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Théo LAGACHE
2026-06-19 15:49:46 +02:00
co-authored by Claude Sonnet 4.6
parent fc42afe89e
commit 68cd89fbe1
2 changed files with 65 additions and 44 deletions
@@ -93,6 +93,7 @@ export const applyOnboardingDbSettingsAction = userAction
}; };
const applyScheduling = async () => { const applyScheduling = async () => {
if (backupMethod === undefined) return;
// Direct DB update — intentionally skips the side effect in // Direct DB update — intentionally skips the side effect in
// updateDatabaseBackupPolicyAction that deletes retention policy on null. // updateDatabaseBackupPolicyAction that deletes retention policy on null.
const cronValue = const cronValue =
@@ -105,12 +106,13 @@ export const applyOnboardingDbSettingsAction = userAction
const applyNotifications = async () => { const applyNotifications = async () => {
if (!notificationPolicies) return; if (!notificationPolicies) return;
await db await db.transaction(async (tx) => {
await tx
.delete(drizzleDb.schemas.alertPolicy) .delete(drizzleDb.schemas.alertPolicy)
.where(eq(drizzleDb.schemas.alertPolicy.databaseId, databaseId)); .where(eq(drizzleDb.schemas.alertPolicy.databaseId, databaseId));
if (notificationPolicies.length > 0) { if (notificationPolicies.length > 0) {
await db.insert(drizzleDb.schemas.alertPolicy).values( await tx.insert(drizzleDb.schemas.alertPolicy).values(
notificationPolicies.map((p) => ({ notificationPolicies.map((p) => ({
databaseId, databaseId,
notificationChannelId: p.channelId, notificationChannelId: p.channelId,
@@ -119,16 +121,18 @@ export const applyOnboardingDbSettingsAction = userAction
})) }))
); );
} }
});
}; };
const applyStorage = async () => { const applyStorage = async () => {
if (!storagePolicies) return; if (!storagePolicies) return;
await db await db.transaction(async (tx) => {
await tx
.delete(drizzleDb.schemas.storagePolicy) .delete(drizzleDb.schemas.storagePolicy)
.where(eq(drizzleDb.schemas.storagePolicy.databaseId, databaseId)); .where(eq(drizzleDb.schemas.storagePolicy.databaseId, databaseId));
if (storagePolicies.length > 0) { if (storagePolicies.length > 0) {
await db.insert(drizzleDb.schemas.storagePolicy).values( await tx.insert(drizzleDb.schemas.storagePolicy).values(
storagePolicies.map((p) => ({ storagePolicies.map((p) => ({
databaseId, databaseId,
storageChannelId: p.channelId, storageChannelId: p.channelId,
@@ -136,6 +140,7 @@ export const applyOnboardingDbSettingsAction = userAction
})) }))
); );
} }
});
}; };
if (section === "retention" || section === "all") await applyRetention(); if (section === "retention" || section === "all") await applyRetention();
@@ -497,7 +497,7 @@ const NotificationsSection = ({
</Button> </Button>
<Button <Button
type="button" type="button"
disabled={isPending} disabled={isPending || policies.some((p) => !p.channelId || p.eventKinds.length === 0)}
onClick={() => onSave(policies)} onClick={() => onSave(policies)}
className="ml-auto" className="ml-auto"
> >
@@ -666,7 +666,7 @@ const StorageSection = ({
</Button> </Button>
<Button <Button
type="button" type="button"
disabled={isPending} disabled={isPending || policies.some((p) => !p.channelId)}
onClick={() => onSave(policies)} onClick={() => onSave(policies)}
className="ml-auto" className="ml-auto"
> >
@@ -803,26 +803,42 @@ export const StepDbSettings = () => {
const handleApplyToAll = async () => { const handleApplyToAll = async () => {
const otherDbIds = databaseIds.filter((id) => id !== dbId); const otherDbIds = databaseIds.filter((id) => id !== dbId);
const succeededIds: string[] = [];
for (const targetId of otherDbIds) { for (const targetId of otherDbIds) {
try {
await applyMutation.mutateAsync({ await applyMutation.mutateAsync({
databaseId: targetId, databaseId: targetId,
section: "all", section: "all",
retention: settings.retention, retention: settings.retention,
backupMethod: settings.backupMethod, backupMethod: settings.backupMethod,
backupCron: settings.backupCron, backupCron: settings.backupCron,
notificationPolicies: settings.notificationPolicies as any, notificationPolicies: settings.notificationPolicies,
storagePolicies: settings.storagePolicies, storagePolicies: settings.storagePolicies,
}); });
succeededIds.push(targetId);
} catch {
// mutation onError already shows a toast
} }
}
if (succeededIds.length > 0) {
const updatedSettings = { ...dbSettings }; const updatedSettings = { ...dbSettings };
otherDbIds.forEach((id) => { succeededIds.forEach((id) => {
updatedSettings[id] = { ...(dbSettings[id] ?? {}), ...settings }; updatedSettings[id] = { ...(dbSettings[id] ?? {}), ...settings };
}); });
await updateContext({ await updateContext({
flowData: { ...state?.context.flowData, dbSettings: updatedSettings }, flowData: { ...state?.context.flowData, dbSettings: updatedSettings },
}); });
}
if (succeededIds.length === otherDbIds.length) {
toast.success("Settings applied to all databases."); toast.success("Settings applied to all databases.");
setPhase({ kind: "grid" }); setPhase({ kind: "grid" });
} else if (succeededIds.length > 0) {
toast.warning(`Settings applied to ${succeededIds.length} of ${otherDbIds.length} databases.`);
setPhase({ kind: "grid" });
}
}; };
const SECTIONS: { const SECTIONS: {