From 97ffd5a72a1cf23397547c92669b649e5c04fc91 Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Fri, 7 Aug 2026 10:51:25 +0200 Subject: [PATCH] fix(frontend): run the Supabase OAuth leg in a popup A full-page redirect unmounts the wizard, so anything the user does on Supabase's side -- signing in, confirming an email, browsing their dashboard -- leaves them with nothing pointing back at Windmill, and the wizard had to park its state in sessionStorage to survive the trip. Open the connect endpoint in a popup instead. The modal stays on screen throughout and the callback hands the token back through postMessage rather than navigating. The parked-state path stays as the fallback for browsers that block the popup. Co-Authored-By: Claude Opus 5 (1M context) --- .../AddDataTableWizard.svelte | 71 ++++++++++++++++--- .../oauth/callback_supabase/+page.svelte | 7 ++ 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/frontend/src/lib/components/workspaceSettings/AddDataTableWizard.svelte b/frontend/src/lib/components/workspaceSettings/AddDataTableWizard.svelte index fd62f3f934..70d30e9ff5 100644 --- a/frontend/src/lib/components/workspaceSettings/AddDataTableWizard.svelte +++ b/frontend/src/lib/components/workspaceSettings/AddDataTableWizard.svelte @@ -81,6 +81,7 @@ let step = $state(1) let provider: Provider | undefined = $state(undefined) let supaMode: 'create' | 'existing' = $state('create') + let supaModeChosen = $state(false) let orgs: SupabaseOrg[] | undefined = $state(undefined) let selectedOrg: string | undefined = $state(undefined) @@ -169,17 +170,51 @@ orgs = await listSupabaseOrgs(t) if (orgs?.length && !selectedOrg) selectedOrg = orgSlug(orgs[0]) projects = await listSupabaseProjects(t) + // Someone who already has a Supabase database almost always means to connect it + // rather than make a second one. Only pre-empt the choice they have not made yet: + // a resumed run was already mid-creation, and a manual pick stands. + if (!resume && !supaModeChosen && projects?.length) supaMode = 'existing' } catch (err) { sendUserToast(String(err), true) orgs = orgs ?? [] } } + const OAUTH_WINDOW = 'windmill_supabase_oauth' + + let oauthWindow: Window | null = null + let oauthPending = $state(false) + + /** + * A full-page redirect unmounts the wizard, so a user who stops to create a Supabase + * account lands on their dashboard with nothing left pointing back here. Driving the flow + * from a popup keeps this modal on screen, and keeps the window ours to steer: after they + * sign up we send the same popup back through the connect endpoint and consent follows. + */ function startOauth() { - parkWizard({ name: dataTableName, region, projectName }) - window.location.href = `${base}/api/oauth/connect/supabase_wizard` + const url = `${base}/api/oauth/connect/supabase_wizard` + oauthWindow = window.open(url, OAUTH_WINDOW, 'width=600,height=820') + if (!oauthWindow) { + // Popups blocked: fall back to the redirect, parking what the user had chosen. + parkWizard({ name: dataTableName, region, projectName }) + window.location.href = url + return + } + oauthPending = true + step = 2 } + $effect(() => { + function onMessage(e: MessageEvent) { + if (e.origin !== window.location.origin || e.data?.type !== 'supabase_oauth') return + $oauthStore = e.data.res + oauthPending = false + oauthWindow?.close() + } + window.addEventListener('message', onMessage) + return () => window.removeEventListener('message', onMessage) + }) + /** * The variable and the resource share a path, so both have to be free. Reusing a taken one * fails *after* the Supabase project exists, stranding a database whose generated password @@ -349,6 +384,7 @@ step = 1 provider = undefined supaMode = 'create' + supaModeChosen = false orgs = undefined projects = undefined selectedOrg = undefined @@ -389,8 +425,15 @@ } if (step === 2) { if (provider === 'supabase') { - // Only reachable if the redirect came back without a token. - if (!authed) return { label: 'Connect to Supabase', disabled: false, act: startOauth } + // Reached while the popup is still open, or if the redirect came back without a + // token. Either way the action is the same: send the popup through consent again, + // which is immediate once the user has an account and is signed in. + if (!authed) + return { + label: oauthPending ? 'Continue' : 'Connect to Supabase', + disabled: false, + act: startOauth + } if (supaMode === 'create') { if (provisioning === 0) return { @@ -487,14 +530,26 @@ {:else if step === 2} {#if provider === 'supabase' && !authed} - - Windmill is not connected to Supabase. Connect to continue. + + {#if oauthPending} + Sign in and approve Windmill in the Supabase window, then come back here. + {:else} + Windmill needs your approval on Supabase to see your databases. + {/if} {:else if provider === 'supabase'} - + supaMode, + (v) => { + supaMode = v + supaModeChosen = true + } + } + > {#snippet children({ item })} - + {/snippet} {#if supaMode === 'create'} diff --git a/frontend/src/routes/oauth/callback_supabase/+page.svelte b/frontend/src/routes/oauth/callback_supabase/+page.svelte index bc0a2a746c..96ffa9b721 100644 --- a/frontend/src/routes/oauth/callback_supabase/+page.svelte +++ b/frontend/src/routes/oauth/callback_supabase/+page.svelte @@ -26,6 +26,13 @@ clientName: client_name, requestBody: { code, state } }) + // Opened as the data table wizard's popup: hand the token to the tab that is still + // sitting on the wizard and get out of the way, so nothing has to be resumed. + if (window.opener) { + window.opener.postMessage({ type: 'supabase_oauth', res }, window.location.origin) + window.close() + return + } $oauthStore = res // The data table wizard parks its state before redirecting; send the user back to // where they started rather than always to /resources.