From 0324f3422ade62cdf38ac0ff7578ff2beecae4df Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:08:00 -0400 Subject: [PATCH] chore(studio): polish vercel deploy-button new project interstitial (#48113) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Studio UI polish for the Vercel deploy-button new-project connect flow (DEPR-616 follow-up). ## What is the current behavior? - Deploy-button project creation still used `VercelIntegrationWindowLayout` (top bar + Docs/Support footer) while install and choose-project already use shared `InterstitialLayout` - The remove-integration note sat as a floating admonition above the form - Advanced / internal-only config sections could show double dividers, and the Oriole radio bottom border could clip inside the collapsible ## What is the new behavior? - Deploy-button new-project uses `InterstitialLayout` with `VercelIntegrationLogo` and `VercelIntegrationFooter`, matching the other Vercel connect surfaces - Regular `/new` project creation is unchanged; Panel chrome is only flattened when `isVercelIntegrationFlow` is set - Removes unused `VercelIntegrationWindowLayout` / `IntegrationWindowLayout` - Fixes Advanced/Internal-only dividers and collapsible border clipping | Before | After | | --- | --- | | Supabase | Create Vercel Project Supabase | ## Additional context ### Testing With Studio running locally and while signed in: 1. Open `http://localhost:8082/integrations/vercel//deploy-button/new-project` 2. Confirm the interstitial card: Vercel + Supabase logo pair, “Create a new project” title, form fields, and the muted remove-integration footer under the card 3. Confirm there is no old window chrome (no “Marketplace Connector” / “Deploy Button” top bar, no Docs/Support footer) 4. Optionally submit and confirm project creation still works 5. Spot-check `/new` to confirm the normal project creation form is unchanged If you have advanced config enabled, expand Advanced Configuration and confirm single dividers (not doubles) and that the Oriole option’s bottom border is not clipped. ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added configurable width to the shared interstitial layout for Vercel project creation. * Updated the Vercel “new project” flow to use the interstitial UI. * **Bug Fixes** * Prevented child borders/shadows from being clipped in expandable configuration sections. * **Refactor** * Removed legacy Vercel/window layout components and updated routing to rely on the interstitial flow. * Switched advanced and internal-only configuration sections to render inside panel content. * **Documentation** * Refreshed Vercel integration route guidance in the migration checklist and inline route comments. --------- Co-authored-by: Joshen Lim --- .../studio/.github/eslint-rule-baselines.json | 2 - apps/studio/TANSTACK_MIGRATION.md | 2 +- .../ProjectCreation/AdvancedConfiguration.tsx | 177 +++++++++--------- .../InternalOnlyConfiguration.tsx | 139 +++++++------- .../ProjectCreation/ProjectCreationForm.tsx | 24 ++- .../IntegrationWindowLayout.tsx | 97 ---------- .../VercelIntegrationWindowLayout.tsx | 36 ---- .../components/layouts/InterstitialLayout.tsx | 7 +- .../[slug]/deploy-button/new-project.tsx | 45 +++-- apps/studio/routes/integrations/vercel.tsx | 8 +- .../$slug/deploy-button/new-project.tsx | 9 +- .../src/CollapsibleCardSection.tsx | 3 +- 12 files changed, 211 insertions(+), 338 deletions(-) delete mode 100644 apps/studio/components/layouts/IntegrationsLayout/IntegrationWindowLayout.tsx delete mode 100644 apps/studio/components/layouts/IntegrationsLayout/VercelIntegrationWindowLayout.tsx diff --git a/apps/studio/.github/eslint-rule-baselines.json b/apps/studio/.github/eslint-rule-baselines.json index 97ba9ff3652..47a1447a26b 100644 --- a/apps/studio/.github/eslint-rule-baselines.json +++ b/apps/studio/.github/eslint-rule-baselines.json @@ -742,8 +742,6 @@ "components/layouts/DatabaseLayout/DatabaseLayout.tsx": 1, "components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx": 1, "components/layouts/EdgeFunctionsLayout/EdgeFunctionsLayout.tsx": 1, - "components/layouts/IntegrationsLayout/IntegrationWindowLayout.tsx": 1, - "components/layouts/IntegrationsLayout/VercelIntegrationWindowLayout.tsx": 1, "components/layouts/JWTKeys/JWTKeysLayout.tsx": 1, "components/layouts/LogsLayout/LogsLayout.tsx": 1, "components/layouts/Navigation/NavigationBar/MobileNavigationBar.tsx": 1, diff --git a/apps/studio/TANSTACK_MIGRATION.md b/apps/studio/TANSTACK_MIGRATION.md index 728290838b2..5afb7202e9e 100644 --- a/apps/studio/TANSTACK_MIGRATION.md +++ b/apps/studio/TANSTACK_MIGRATION.md @@ -78,7 +78,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp - [x] `routes/_app/account.tsx` — AccountLayout (reads `accountLayoutTitle` from leaf `staticData`) - [x] `routes/_app/org.tsx` — OrganizationLayout (reads `orgLayoutTitle` from leaf `staticData`). **Delta vs plan:** placed at `_app/org.tsx` (wraps both `/org/` index and `/org/$slug/*`) instead of `_app/org/$slug.tsx`. PageLayout stays inline on `/org/$slug/index.tsx` since only that one route uses it. - [x] `routes/_app/new.tsx` — skipped; only `_app/new/index.tsx` lives under \_app (inlines WizardLayout). `new/$slug` is top-level (no AppLayout) so a sub-shell would not actually share state. -- [x] `routes/integrations/vercel.tsx` — VercelIntegrationWindowLayout. **Delta vs plan:** placed at top-level rather than under `_app/` — Next getLayout for all three leaves wraps only in VercelIntegrationWindowLayout, no AppLayout/DefaultLayout. +- [x] `routes/integrations/vercel.tsx` — passthrough `Outlet` only (no shared window layout). **Delta vs plan:** placed at top-level rather than under `_app/`. All three Vercel leaves (install, marketplace choose-project, deploy-button new-project) render their own `InterstitialLayout` inline; the old `VercelIntegrationWindowLayout` was removed. ### Project shell diff --git a/apps/studio/components/interfaces/ProjectCreation/AdvancedConfiguration.tsx b/apps/studio/components/interfaces/ProjectCreation/AdvancedConfiguration.tsx index 9651283d523..c9a621a4d72 100644 --- a/apps/studio/components/interfaces/ProjectCreation/AdvancedConfiguration.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/AdvancedConfiguration.tsx @@ -2,8 +2,6 @@ import { useFlag } from 'common' import { UseFormReturn } from 'react-hook-form' import { Badge, - Card, - CardContent, cn, FormControl, FormField, @@ -20,6 +18,7 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { CreateProjectForm } from './ProjectCreation.schema' import { DocsButton } from '@/components/ui/DocsButton' +import Panel from '@/components/ui/Panel' import { DOCS_URL } from '@/lib/constants' interface AdvancedConfigurationProps { @@ -30,93 +29,91 @@ export const AdvancedConfiguration = ({ form }: AdvancedConfigurationProps) => { const disableOrioleProjectCreation = useFlag('disableOrioleProjectCreation') return ( - - - - ( - <> - - - field.onChange(value === 'true')} - defaultValue={field.value.toString()} - > - - - - Postgres - Default - - } - description="Recommended for production workloads" - className="[&>div>div>p]:text-left [&>div>div>p]:text-xs [&>div>div>label]:flex [&>div>div>label]:items-center [&>div>div>label]:gap-x-2" - /> - - - - - - - - Postgres with OrioleDB - Alpha - - } - description="Not recommended for production workloads" - className={cn( - '[&>div>div>p]:text-left [&>div>div>p]:text-xs [&>div>div>label]:flex [&>div>div>label]:items-center [&>div>div>label]:gap-x-2', - form.getValues('useOrioleDb') ? 'rounded-b-none!' : '' - )} - disabled={disableOrioleProjectCreation} - /> - - {disableOrioleProjectCreation && ( - - OrioleDB is temporarily disabled for new projects. Please try again - later. - - )} - - - - - - {form.getValues('useOrioleDb') && ( - - - - )} - - - )} - /> - - - + + + ( + <> + + + field.onChange(value === 'true')} + defaultValue={field.value.toString()} + > + + + + Postgres + Default + + } + description="Recommended for production workloads" + className="[&>div>div>p]:text-left [&>div>div>p]:text-xs [&>div>div>label]:flex [&>div>div>label]:items-center [&>div>div>label]:gap-x-2" + /> + + + + + + + + Postgres with OrioleDB + Alpha + + } + description="Not recommended for production workloads" + className={cn( + '[&>div>div>p]:text-left [&>div>div>p]:text-xs [&>div>div>label]:flex [&>div>div>label]:items-center [&>div>div>label]:gap-x-2', + form.getValues('useOrioleDb') ? 'rounded-b-none!' : '' + )} + disabled={disableOrioleProjectCreation} + /> + + {disableOrioleProjectCreation && ( + + OrioleDB is temporarily disabled for new projects. Please try again + later. + + )} + + + + + + {form.getValues('useOrioleDb') && ( + + + + )} + + + )} + /> + + ) } diff --git a/apps/studio/components/interfaces/ProjectCreation/InternalOnlyConfiguration.tsx b/apps/studio/components/interfaces/ProjectCreation/InternalOnlyConfiguration.tsx index 3737650ec10..12858f3df5f 100644 --- a/apps/studio/components/interfaces/ProjectCreation/InternalOnlyConfiguration.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/InternalOnlyConfiguration.tsx @@ -1,7 +1,7 @@ import { useParams } from 'common' import { UseFormReturn } from 'react-hook-form' import { type CloudProvider } from 'shared-data' -import { Card, CardContent, FormControl, FormField, Input } from 'ui' +import { FormControl, FormField, Input } from 'ui' import { CollapsibleCardSection } from 'ui-patterns/CollapsibleCardSection' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' @@ -9,6 +9,7 @@ import { CloudProviderSelector } from './CloudProviderSelector' import { HighAvailabilityInput } from './HighAvailabilityInput' import { PostgresVersionSelector } from './PostgresVersionSelector' import { CreateProjectForm } from './ProjectCreation.schema' +import Panel from '@/components/ui/Panel' interface InternalOnlyConfigurationProps { form: UseFormReturn @@ -19,76 +20,74 @@ export const InternalOnlyConfiguration = ({ form }: InternalOnlyConfigurationPro const showNonProdFields = process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod' return ( - - - -
-
- ( - - )} - /> + + +
+
+ ( + + )} + /> - -
- - {showNonProdFields && ( -
-

- The settings below are only applicable for local/staging projects -

-
- - - ( - - - - - - )} - /> - - ( - - - - - - )} - /> -
-
- )} +
-
- - + + {showNonProdFields && ( +
+

+ The settings below are only applicable for local/staging projects +

+
+ + + ( + + + + + + )} + /> + + ( + + + + + + )} + /> +
+
+ )} +
+ + ) } diff --git a/apps/studio/components/interfaces/ProjectCreation/ProjectCreationForm.tsx b/apps/studio/components/interfaces/ProjectCreation/ProjectCreationForm.tsx index 75516031101..cd455528204 100644 --- a/apps/studio/components/interfaces/ProjectCreation/ProjectCreationForm.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/ProjectCreationForm.tsx @@ -8,7 +8,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { useForm, useFormState } from 'react-hook-form' import { type CloudProvider } from 'shared-data' import { toast } from 'sonner' -import { Button, Form, useWatch } from 'ui' +import { Button, cn, Form, useWatch } from 'ui' import { Admonition } from 'ui-patterns/admonition' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { z } from 'zod' @@ -92,6 +92,8 @@ interface ProjectCreationFormProps { * - "Cancel" button * - Shows the following: * - "Data seeding" section + * - When embedded in the Vercel interstitial, flattens Panel chrome so the shared + * form fields sit inside InterstitialLayout without a nested card * Eventually we could looking into reducing the differences more, e.g having data seeding * for both ways, and showing GitHub repository field for Vercel integration */ @@ -581,14 +583,20 @@ export const ProjectCreationForm = ({ > -

Create a new project

-

- Your project will have its own dedicated instance and full Postgres database. An API - will be set up so you can easily interact with your new database. -

-
+ !isVercelIntegrationFlow && ( +
+

Create a new project

+

+ Your project will have its own dedicated instance and full Postgres database. An + API will be set up so you can easily interact with your new database. +

+
+ ) } footer={ ) => { - return ( -
-
- -
{children}
- - {docsHref && ( - - - Docs - - )} - - - Support - - -
- ) -} - -const INTEGRATION_LAYOUT_MAX_WIDTH = '' // 'max-w-[720px]' - -export default withAuth(IntegrationWindowLayout) - -export const IntegrationWindowLayoutWithoutAuth = IntegrationWindowLayout - -export type HeaderProps = { - title: string - integrationIcon: ReactNode -} - -const Header = ({ title, integrationIcon }: HeaderProps) => { - return ( -
- -
-
-
- Supabase -
- - {integrationIcon} -
- - {title} - -
-
-
- ) -} - -const maxWidthClasses = 'mx-auto w-full max-w-[1600px]' -const paddingClasses = 'px-6 lg:px-14 xl:px-28 2xl:px-32' - -export const IntegrationScaffoldContainer = forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => { - return
-}) - -IntegrationScaffoldContainer.displayName = 'IntegrationScaffoldContainer' diff --git a/apps/studio/components/layouts/IntegrationsLayout/VercelIntegrationWindowLayout.tsx b/apps/studio/components/layouts/IntegrationsLayout/VercelIntegrationWindowLayout.tsx deleted file mode 100644 index 96df39b67b1..00000000000 --- a/apps/studio/components/layouts/IntegrationsLayout/VercelIntegrationWindowLayout.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useParams } from 'common' -import { PropsWithChildren } from 'react' -import InlineSVG from 'react-inlinesvg' - -import IntegrationWindowLayout from './IntegrationWindowLayout' -import { BASE_PATH } from '@/lib/constants' -import { useIntegrationInstallationSnapshot } from '@/state/integration-installation' - -const VERCEL_ICON = ( -
- -
-) - -const VercelIntegrationWindowLayout = ({ children }: PropsWithChildren<{}>) => { - const { externalId } = useParams() - - const snapshot = useIntegrationInstallationSnapshot() - - const title = externalId - ? 'Supabase + Vercel Deploy Button' - : 'Supabase + Vercel Integration Marketplace Connector' - - return ( - - {children} - - ) -} - -export default VercelIntegrationWindowLayout diff --git a/apps/studio/components/layouts/InterstitialLayout.tsx b/apps/studio/components/layouts/InterstitialLayout.tsx index fa61322feee..d17b469956e 100644 --- a/apps/studio/components/layouts/InterstitialLayout.tsx +++ b/apps/studio/components/layouts/InterstitialLayout.tsx @@ -16,6 +16,8 @@ interface InterstitialLayoutProps { footer?: ReactNode containerClassName?: string cardClassName?: string + /** Shared max-width for the card and footer column. Defaults to `max-w-[400px]`. */ + widthClassName?: string titleClassName?: string descriptionClassName?: string } @@ -34,6 +36,7 @@ export const InterstitialLayout = ({ footer, containerClassName, cardClassName, + widthClassName = 'max-w-[400px]', titleClassName, descriptionClassName, children, @@ -67,7 +70,7 @@ export const InterstitialLayout = ({ {(logo || title || description) && ( @@ -92,7 +95,7 @@ export const InterstitialLayout = ({ )} > {footer ? ( -
+
{card}
{footer}
diff --git a/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx b/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx index 2e1f3b8d277..02e1c050ab2 100644 --- a/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx +++ b/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx @@ -1,19 +1,29 @@ import { useParams } from 'common' +import Head from 'next/head' import { useEffect, useState } from 'react' -import { Admonition } from 'ui-patterns/admonition' import { isVercelUrl } from '@/components/interfaces/Integrations/Vercel/VercelIntegration.utils' +import { + VercelIntegrationFooter, + VercelIntegrationLogo, +} from '@/components/interfaces/Integrations/Vercel/VercelIntegrationInterstitial' import { ProjectCreationForm } from '@/components/interfaces/ProjectCreation/ProjectCreationForm' -import VercelIntegrationWindowLayout from '@/components/layouts/IntegrationsLayout/VercelIntegrationWindowLayout' -import { ScaffoldColumn, ScaffoldContainer } from '@/components/layouts/Scaffold' +import { InterstitialLayout } from '@/components/layouts/InterstitialLayout' import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query' import { useIntegrationsQuery } from '@/data/integrations/integrations-query' import { useIntegrationVercelConnectionsCreateMutation } from '@/data/integrations/integrations-vercel-connections-create-mutation' import { useVercelProjectsQuery } from '@/data/integrations/integrations-vercel-projects-query' import { useOrganizationsQuery } from '@/data/organizations/organizations-query' +import { withAuth } from '@/hooks/misc/withAuth' +import { buildStudioPageTitle } from '@/lib/page-title' import { useIntegrationInstallationSnapshot } from '@/state/integration-installation' import type { NextPageWithLayout } from '@/types' +const PAGE_TITLE = buildStudioPageTitle({ + section: 'Create Vercel Project', + brand: 'Supabase', +}) + const VercelIntegration: NextPageWithLayout = () => { const { slug, next, currentProjectId: foreignProjectId } = useParams() const snapshot = useIntegrationInstallationSnapshot() @@ -91,23 +101,22 @@ const VercelIntegration: NextPageWithLayout = () => { }, [data, isSuccess]) return ( - - - + <> + + {PAGE_TITLE} + + } + title="Create a new project" + description="Your project will have its own dedicated instance and full Postgres database. An API will be set up so you can easily interact with your new database." + footer={} + widthClassName="max-w-2xl" + > - - + + ) } -VercelIntegration.getLayout = (page) => ( - {page} -) - -export default VercelIntegration +export default withAuth(VercelIntegration) diff --git a/apps/studio/routes/integrations/vercel.tsx b/apps/studio/routes/integrations/vercel.tsx index e31fc665ab9..8e9bcd49623 100644 --- a/apps/studio/routes/integrations/vercel.tsx +++ b/apps/studio/routes/integrations/vercel.tsx @@ -4,11 +4,9 @@ export const Route = createFileRoute('/integrations/vercel')({ component: VercelIntegrationPassthrough, }) -// No shared layout here. Since #47623, the install and -// marketplace/choose-project pages render their own InterstitialLayout and -// have no Next getLayout, so the Next runtime shows them without any window -// chrome. Only deploy-button/new-project still uses -// VercelIntegrationWindowLayout, and its route wraps it at the leaf. +// No shared layout here. Since #47623, the Vercel install, marketplace +// choose-project, and deploy-button/new-project pages each render their own +// InterstitialLayout and have no Next getLayout / window chrome. function VercelIntegrationPassthrough() { return } diff --git a/apps/studio/routes/integrations/vercel/$slug/deploy-button/new-project.tsx b/apps/studio/routes/integrations/vercel/$slug/deploy-button/new-project.tsx index 0b09a4cf7e9..11f685c8208 100644 --- a/apps/studio/routes/integrations/vercel/$slug/deploy-button/new-project.tsx +++ b/apps/studio/routes/integrations/vercel/$slug/deploy-button/new-project.tsx @@ -1,18 +1,11 @@ import { createFileRoute } from '@tanstack/react-router' -import VercelIntegrationWindowLayout from '@/components/layouts/IntegrationsLayout/VercelIntegrationWindowLayout' import VercelIntegration from '@/pages/integrations/vercel/[slug]/deploy-button/new-project' export const Route = createFileRoute('/integrations/vercel/$slug/deploy-button/new-project')({ component: VercelDeployButtonNewProjectRoute, }) -// Mirrors the page's Next getLayout, which wraps this leaf (and only this -// leaf) in VercelIntegrationWindowLayout. function VercelDeployButtonNewProjectRoute() { - return ( - - - - ) + return } diff --git a/packages/ui-patterns/src/CollapsibleCardSection.tsx b/packages/ui-patterns/src/CollapsibleCardSection.tsx index 17e3268a76a..a0d3df4ceb1 100644 --- a/packages/ui-patterns/src/CollapsibleCardSection.tsx +++ b/packages/ui-patterns/src/CollapsibleCardSection.tsx @@ -25,7 +25,8 @@ export const CollapsibleCardSection = ({ {description &&

{description}

}