Files
supabase/apps/studio/components/interfaces/ProjectCreation/ProjectCreationForm.tsx
T
Danny WhiteandJoshen Lim 0324f3422a chore(studio): polish vercel deploy-button new project interstitial (#48113)
## 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 |
| --- | --- |
| <img width="965" height="1107" alt="Supabase"
src="https://github.com/user-attachments/assets/99c1225c-c9e5-4fac-bd64-17920403e447"
/> | <img width="965" height="1107" alt="Create Vercel Project Supabase"
src="https://github.com/user-attachments/assets/c42bff96-5f9f-4c8b-8f3c-093e380f90ed"
/> |

## Additional context

### Testing

With Studio running locally and while signed in:

1. Open
`http://localhost:8082/integrations/vercel/<your-org-slug>/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.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-07-22 00:08:00 +10:00

763 lines
30 KiB
TypeScript

import { zodResolver } from '@hookform/resolvers/zod'
import { acceptUntrustedSql, joinSqlFragments, untrustedSql } from '@supabase/pg-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useFeatureFlags, useFlag, useParams } from 'common'
import Link from 'next/link'
import { useRouter } from 'next/router'
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, cn, Form, useWatch } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { z } from 'zod'
import { AdvancedConfiguration } from './AdvancedConfiguration'
import { ComputeSizeSelector } from './ComputeSizeSelector'
import { DatabasePasswordInput } from './DatabasePasswordInput'
import { DataSeeding } from './DataSeeding'
import { DisabledWarningDueToIncident } from './DisabledWarningDueToIncident'
import { FreeProjectLimitWarning } from './FreeProjectLimitWarning'
import { InternalOnlyConfiguration } from './InternalOnlyConfiguration'
import { OrganizationSelector } from './OrganizationSelector'
import { extractPostgresVersionDetails } from './PostgresVersionSelector'
import { sizes } from './ProjectCreation.constants'
import { FormSchema } from './ProjectCreation.schema'
import {
instanceLabel,
monthlyInstancePrice,
smartRegionToExactRegion,
} from './ProjectCreation.utils'
import { ProjectCreationFooter } from './ProjectCreationFooter'
import { ProjectNameInput } from './ProjectNameInput'
import { RegionSelector } from './RegionSelector'
import { SecurityOptions } from './SecurityOptions'
import { AUTO_ENABLE_RLS_EVENT_TRIGGER_SQL } from '@/components/interfaces/Database/Triggers/EventTriggersList/EventTriggers.constants'
import {
GitHubRepositoryField,
useGitHubRepositoryOptions,
} from '@/components/interfaces/Settings/Integrations/GithubIntegration/GitHubRepositoryField'
import Panel from '@/components/ui/Panel'
import { useAvailableOrioleImageVersion } from '@/data/config/project-creation-postgres-versions-query'
import { useOverdueInvoicesQuery } from '@/data/invoices/invoices-overdue-query'
import { useDefaultRegionQuery } from '@/data/misc/get-default-region-query'
import { useAuthorizedAppsQuery } from '@/data/oauth/authorized-apps-query'
import { useFreeProjectLimitCheckQuery } from '@/data/organizations/free-project-limit-check-query'
import { useOrganizationAvailableRegionsQuery } from '@/data/organizations/organization-available-regions-query'
import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
import { DesiredInstanceSize } from '@/data/projects/new-project.constants'
import {
OrgProject,
useOrgProjectsInfiniteQuery,
} from '@/data/projects/org-projects-infinite-query'
import {
ProjectCreateVariables,
useProjectCreateMutation,
} from '@/data/projects/project-create-mutation'
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import {
isInDataApiRevokeTreatment,
useDataApiRevokeOnCreateDefaultEnabled,
} from '@/hooks/misc/useDataApiRevokeOnCreateDefault'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { usePHFlag } from '@/hooks/ui/useFlag'
import { DOCS_URL, PROJECT_STATUS, PROVIDERS, useDefaultProvider } from '@/lib/constants'
import { getInitialMigrationSQLFromGitHubRepo } from '@/lib/integration-utils'
import { useProfile } from '@/lib/profile'
import { trimSafeSqlFragment } from '@/lib/sql'
import { classifyApiError, classifyValidationError } from '@/lib/telemetry/funnel-errors'
import { useTrack } from '@/lib/telemetry/track'
import { useTrackFunnelError } from '@/lib/telemetry/use-track-funnel-error'
const sizesWithNoCostConfirmationRequired: DesiredInstanceSize[] = ['micro', 'small']
interface ProjectCreationFormProps {
isVercelIntegrationFlow?: boolean
onCreateSuccess?: (ref: string) => void
}
/**
* [Joshen] JFYI am only adding the `isVercelIntegrationFlow` flag to keep the existing
* behaviour for project creation via Vercel integration as similar to keep current state
* for now, what it controls if `true`:
* - Disables organization selection
* - Hides the following:
* - "Internal configuration" section
* - "GitHub repository" field
* - "Free project info" at the bottom
* - "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
*/
export const ProjectCreationForm = ({
isVercelIntegrationFlow = false,
onCreateSuccess,
}: ProjectCreationFormProps) => {
const track = useTrack()
const router = useRouter()
const { profile } = useProfile()
const { slug, projectName, externalId } = useParams()
const trackFunnelError = useTrackFunnelError()
const defaultProvider = useDefaultProvider()
const surface = isVercelIntegrationFlow ? 'vercel' : 'main'
const { data: currentOrg } = useSelectedOrganizationQuery()
const isFreePlan = currentOrg?.plan?.id === 'free'
const canChooseInstanceSize = !isFreePlan
const { lastVisitedOrganization } = useLastVisitedOrganization()
const { can: isAdmin } = useAsyncCheckPermissions(PermissionAction.CREATE, 'projects')
const { can: canCreateGitHubConnection } = useAsyncCheckPermissions(
PermissionAction.CREATE,
'integrations.github_connections'
)
const showAdvancedConfig = useIsFeatureEnabled('project_creation:show_advanced_config')
const { hasAccess: hasAccessToGitHubIntegration } = useCheckEntitlements(
'integrations.github_connections'
)
const { hasLoaded: flagsLoaded } = useFeatureFlags()
const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate')
const showInternalOnlyConfiguration =
useFlag('newProjectInternalOnlyConfiguration') && !isVercelIntegrationFlow
// Read the raw flag for telemetry — coerce-undefined-to-false would record false for
// users whose flags haven't loaded yet. The raw value preserves undefined (omitted from
// PostHog) so we only record an actual value (boolean true/false, or a variant string
// like 'test'/'control' post-multivariate migration) once the flag has resolved.
const dataApiRevokeOnCreateDefaultFlag = usePHFlag<boolean | string>(
'dataApiRevokeOnCreateDefault'
)
const isDataApiRevokeOnCreateDefault = useDataApiRevokeOnCreateDefaultEnabled()
const isNotOnHigherPlan = !['team', 'enterprise', 'platform'].includes(currentOrg?.plan.id ?? '')
const [allProjects, setAllProjects] = useState<OrgProject[] | undefined>(undefined)
const [isComputeCostsConfirmationModalVisible, setIsComputeCostsConfirmationModalVisible] =
useState(false)
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
mode: 'onChange',
defaultValues: {
organization: slug,
projectName: projectName || '',
highAvailability: false,
postgresVersion: '',
instanceType: '',
cloudProvider: PROVIDERS[defaultProvider].id,
dbPass: '',
dbPassStrength: 0,
dbPassStrengthMessage: '',
dbRegion: undefined,
githubRepositoryId: '',
githubInstallationId: undefined,
githubRepositoryName: '',
instanceSize: canChooseInstanceSize ? sizes[0] : undefined,
dataApi: true,
dataApiDefaultPrivileges: !isDataApiRevokeOnCreateDefault,
enableRlsEventTrigger: false,
postgresVersionSelection: '',
useOrioleDb: false,
shouldRunMigrations: true,
},
})
const { getFieldState, resetField, setValue } = form
const {
instanceSize: watchedInstanceSize,
cloudProvider,
dbRegion,
githubRepositoryName,
organization,
projectName: watchedProjectName,
highAvailability,
} = useWatch({ control: form.control })
const { dirtyFields } = useFormState(form)
const isDbRegionDirty = dirtyFields.dbRegion
const smartRegionEnabled = cloudProvider !== 'AWS_NIMBUS'
// Read dirty state during render rather than depending on form.formState in the
// effect — form.formState is a Proxy that gets a new reference every render, which
// would re-fire this effect after each setValue and trigger an infinite loop.
const isDataApiDefaultPrivilegesDirty = getFieldState(
'dataApiDefaultPrivileges',
form.formState
).isDirty
// [Charis] Since the form is updated in a useEffect, there is an edge case
// when switching from free to paid, where canChooseInstanceSize is true for
// an in-between render, but watchedInstanceSize is still undefined from the
// form state carried over from the free plan. To avoid this, we set a
// default instance size in this case.
const instanceSize = canChooseInstanceSize ? (watchedInstanceSize ?? sizes[0]) : undefined
const { data: membersExceededLimit = [] } = useFreeProjectLimitCheckQuery(
{ slug },
{ enabled: isFreePlan }
)
const hasMembersExceedingFreeTierLimit = membersExceededLimit.length > 0
const freePlanWithExceedingLimits = isFreePlan && hasMembersExceedingFreeTierLimit
const { data: organizations = [], isSuccess: isOrganizationsSuccess } = useOrganizationsQuery()
const isEmptyOrganizations = isOrganizationsSuccess && organizations.length <= 0
const { data: approvedOAuthApps = [] } = useAuthorizedAppsQuery(
{ slug },
{ enabled: !isFreePlan && slug !== '_' }
)
const hasOAuthApps = approvedOAuthApps.length > 0
const { data: allOverdueInvoices = [] } = useOverdueInvoicesQuery({
enabled: isNotOnHigherPlan,
})
const overdueInvoices = allOverdueInvoices.filter((x) => x.organization_id === currentOrg?.id)
const hasOutstandingInvoices = isNotOnHigherPlan && overdueInvoices.length > 0
const { data: orgProjectsFromApi } = useOrgProjectsInfiniteQuery({ slug: currentOrg?.slug })
const allOrgProjects = useMemo(
() => orgProjectsFromApi?.pages.flatMap((page) => page.projects),
[orgProjectsFromApi?.pages]
)
const organizationProjects =
allProjects?.filter((project) => project.status !== PROJECT_STATUS.INACTIVE) ?? []
const availableComputeCredits = organizationProjects.length === 0 ? 10 : 0
const additionalMonthlySpend = isFreePlan
? 0
: monthlyInstancePrice(instanceSize) - availableComputeCredits
const selectedCloudProvider = cloudProvider as CloudProvider
const { data: autoDefaultRegion, error: defaultRegionError } = useDefaultRegionQuery(
{
cloudProvider: selectedCloudProvider,
},
{
enabled: flagsLoaded && !smartRegionEnabled,
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchInterval: false,
refetchOnReconnect: false,
retry: false,
}
)
const { data: availableRegionsData, error: availableRegionsError } =
useOrganizationAvailableRegionsQuery(
{
slug: slug,
cloudProvider: PROVIDERS[cloudProvider as CloudProvider].id,
desiredInstanceSize: instanceSize as DesiredInstanceSize,
},
{
enabled: flagsLoaded && smartRegionEnabled,
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchInterval: false,
refetchOnReconnect: false,
}
)
const recommendedSmartRegion = smartRegionEnabled
? availableRegionsData?.recommendations.smartGroup.name
: ''
const fixedDefaultRegion = PROVIDERS[selectedCloudProvider].default_region.displayName
const regionError = smartRegionEnabled ? availableRegionsError : defaultRegionError
const defaultRegion = smartRegionEnabled
? availableRegionsData?.recommendations.smartGroup.name
: (autoDefaultRegion ?? fixedDefaultRegion)
const canCreateProject = isAdmin && !freePlanWithExceedingLimits && !hasOutstandingInvoices
const canConfigureGitHubOnCreate =
canCreateProject && hasAccessToGitHubIntegration && canCreateGitHubConnection
const dbRegionExact = smartRegionToExactRegion(dbRegion ?? '')
const availableOrioleVersion = useAvailableOrioleImageVersion(
{
cloudProvider: cloudProvider as CloudProvider,
dbRegion: smartRegionEnabled ? dbRegionExact : (dbRegion ?? ''),
organizationSlug: organization,
},
{ enabled: currentOrg !== null }
)
const userPrimaryEmail = profile?.primary_email?.toLowerCase()
const isUserAtFreeProjectLimit = userPrimaryEmail
? membersExceededLimit.some(
(member) => member.primary_email?.toLowerCase() === userPrimaryEmail
)
: false
const shouldShowFreeProjectInfo =
!!currentOrg && !isFreePlan && !isUserAtFreeProjectLimit && !isVercelIntegrationFlow
const {
gitHubAuthorization,
githubRepos,
hasPartialResponseDueToSSO,
isLoading: isLoadingRepositoryOptions,
refetch: refetchRepositoryOptions,
} = useGitHubRepositoryOptions()
const {
mutate: createProject,
isPending: isCreatingNewProject,
isSuccess: isSuccessNewProject,
} = useProjectCreateMutation({
onSuccess: (res) => {
track(
'project_creation_simple_version_submitted',
{
surface,
instanceSize: form.getValues('instanceSize'),
enableRlsEventTrigger: form.getValues('enableRlsEventTrigger'),
dataApiEnabled: form.getValues('dataApi'),
dataApiDefaultPrivilegesGranted: form.getValues('dataApiDefaultPrivileges'),
useOrioleDb: form.getValues('useOrioleDb'),
...(dataApiRevokeOnCreateDefaultFlag !== undefined && {
dataApiRevokeOnCreateDefaultEnabled: dataApiRevokeOnCreateDefaultFlag,
}),
},
{
project: res.ref,
organization: res.organization_slug,
}
)
onCreateSuccess?.(res.ref)
if (surface === 'main') router.push(`/project/${res.ref}`)
},
onError: (error) => {
const toastId = toast.error(`Failed to create new project: ${error.message}`)
trackFunnelError(
'project_creation',
classifyApiError('project_creation', error),
'toast',
toastId
)
},
})
const onSubmitWithComputeCostsConfirmation = async (values: z.infer<typeof FormSchema>) => {
const launchingLargerInstance =
values.instanceSize &&
!sizesWithNoCostConfirmationRequired.includes(values.instanceSize as DesiredInstanceSize)
if (additionalMonthlySpend > 0 && (hasOAuthApps || launchingLargerInstance)) {
track('project_creation_simple_version_confirm_modal_opened', {
instanceSize: values.instanceSize,
})
setIsComputeCostsConfirmationModalVisible(true)
} else {
await onSubmit(values)
}
}
const onSubmit = async (values: z.infer<typeof FormSchema>) => {
if (!currentOrg) return console.error('Unable to retrieve current organization')
const {
cloudProvider,
projectName,
highAvailability,
dbPass,
dbRegion,
postgresVersion,
instanceType,
instanceSize,
dataApi,
dataApiDefaultPrivileges,
enableRlsEventTrigger,
postgresVersionSelection,
useOrioleDb,
githubInstallationId,
githubRepositoryId,
shouldRunMigrations,
} = values
if (postgresVersion && !postgresVersion.match(/1[2-9]\..*/)) {
return toast.error(
`Invalid Postgres version, should start with a number between 12-19, a dot and additional characters, i.e. 15.2 or 15.2.0-3`
)
}
if (useOrioleDb && !availableOrioleVersion) {
const toastId = toast.error('No available OrioleDB image found, only Postgres is available')
trackFunnelError(
'project_creation',
{ errorCategory: 'validation', errorReason: 'oriole_unavailable' },
'toast',
toastId
)
return
}
const { postgresEngine, releaseChannel } =
extractPostgresVersionDetails(postgresVersionSelection)
const { smartGroup = [], specific = [] } = availableRegionsData?.all ?? {}
const selectedRegion = smartRegionEnabled
? (smartGroup.find((x) => x.name === dbRegion) ?? specific.find((x) => x.name === dbRegion))
: undefined
const parsedGitHubRepositoryId =
githubRepositoryId.length > 0 ? Number(githubRepositoryId) : undefined
const shouldIncludeGitHubFields =
githubInstallationId !== undefined && Number.isFinite(parsedGitHubRepositoryId)
let dbSql = enableRlsEventTrigger ? AUTO_ENABLE_RLS_EVENT_TRIGGER_SQL : undefined
if (isVercelIntegrationFlow && shouldRunMigrations && !!externalId) {
const id = toast.loading(`Fetching initial migrations from GitHub repository...`)
try {
const migrationSql = await getInitialMigrationSQLFromGitHubRepo(externalId)
if (migrationSql) {
const safeMigrationSql = trimSafeSqlFragment(
acceptUntrustedSql(untrustedSql(migrationSql))
)
dbSql = dbSql
? joinSqlFragments([trimSafeSqlFragment(dbSql), safeMigrationSql], ';\n')
: safeMigrationSql
toast.loading(`Migrations fetched! Creating project...`, { id })
} else {
toast.loading('No migrations found, creating project...')
}
} catch (error) {
toast.loading(
`Failed to fetch migrations: ${error instanceof Error ? error.message : ''}. Proceeding to create project...`,
{ id }
)
}
}
const data: ProjectCreateVariables = {
dbSql,
dbPass,
cloudProvider,
organizationSlug: currentOrg.slug,
name: projectName,
highAvailability,
// gets ignored due to org billing subscription anyway
dbPricingTierId: 'tier_free',
// only set the compute size on pro+ plans. Free plans always use micro (nano in the future) size.
dbInstanceSize: isFreePlan ? undefined : (instanceSize as DesiredInstanceSize),
dataApiExposedSchemas: !dataApi ? [] : undefined,
dataApiUseApiSchema: false,
dataApiRevokeDefaultPrivileges: dataApi && !dataApiDefaultPrivileges,
postgresEngine: useOrioleDb ? availableOrioleVersion?.postgres_engine : postgresEngine,
releaseChannel: useOrioleDb ? availableOrioleVersion?.release_channel : releaseChannel,
...(smartRegionEnabled ? { regionSelection: selectedRegion } : { dbRegion }),
...(shouldIncludeGitHubFields
? {
githubInstallationId,
githubRepositoryId: parsedGitHubRepositoryId,
}
: {}),
}
if (postgresVersion || instanceType) {
data['customSupabaseRequest'] = {
ami: {
...(postgresVersion && {
search_tags: { 'tag:postgresVersion': postgresVersion },
}),
...(instanceType && { instance_type: instanceType }),
},
}
}
createProject(data)
}
const hasTrackedFormExposed = useRef(false)
useEffect(() => {
if (hasTrackedFormExposed.current) return
if (!isOrganizationsSuccess || !canCreateProject || !currentOrg) return
hasTrackedFormExposed.current = true
track('project_creation_form_exposed', { surface })
}, [isOrganizationsSuccess, canCreateProject, currentOrg, track, surface])
useEffect(() => {
// Only set once to ensure compute credits dont change while project is being created
if (allOrgProjects && allOrgProjects.length > 0 && !allProjects) {
setAllProjects(allOrgProjects)
}
}, [allOrgProjects, allProjects, setAllProjects])
useEffect(() => {
// Handle no org: redirect to new org route
if (isEmptyOrganizations && !isVercelIntegrationFlow) {
router.push(`/new`)
}
}, [isEmptyOrganizations, isVercelIntegrationFlow, router])
useEffect(() => {
// [Joshen] Cause slug depends on router which doesnt load immediately on render
// While the form data does load immediately
if (slug && slug !== '_') setValue('organization', slug)
if (projectName) setValue('projectName', projectName || '')
}, [slug, setValue, projectName])
useEffect(() => {
if (!isDbRegionDirty && defaultRegion) {
setValue('dbRegion', defaultRegion)
}
}, [defaultRegion, isDbRegionDirty, setValue])
useEffect(() => {
if (!isDbRegionDirty && recommendedSmartRegion) {
setValue('dbRegion', recommendedSmartRegion)
}
}, [recommendedSmartRegion, isDbRegionDirty, setValue])
useEffect(() => {
if (regionError && fixedDefaultRegion) {
resetField('dbRegion', { defaultValue: fixedDefaultRegion })
}
}, [regionError, resetField, fixedDefaultRegion])
useEffect(() => {
if (highAvailability && cloudProvider !== 'AWS_K8S') {
setValue('cloudProvider', 'AWS_K8S')
}
}, [highAvailability, cloudProvider, setValue])
useEffect(() => {
if (watchedInstanceSize !== instanceSize) {
setValue('instanceSize', instanceSize, {
shouldDirty: false,
shouldValidate: false,
shouldTouch: false,
})
}
}, [instanceSize, watchedInstanceSize, setValue])
useEffect(() => {
if (!githubRepositoryName) return
if ((watchedProjectName ?? '').trim().length > 0) return
const repoName = githubRepositoryName.split('/').at(-1) ?? githubRepositoryName
setValue('projectName', repoName.trim(), {
shouldValidate: true,
})
}, [githubRepositoryName, watchedProjectName, setValue])
useEffect(() => {
if (dataApiRevokeOnCreateDefaultFlag === undefined) return
if (isDataApiDefaultPrivilegesDirty) return
setValue(
'dataApiDefaultPrivileges',
!isInDataApiRevokeTreatment(dataApiRevokeOnCreateDefaultFlag),
{
shouldDirty: false,
}
)
}, [dataApiRevokeOnCreateDefaultFlag, isDataApiDefaultPrivilegesDirty, setValue])
useEffect(() => {
// This is to make the database.new redirect work correctly. The database.new redirect should be set to supabase.com/dashboard/new/last-visited-org
if (slug === 'last-visited-org') {
if (lastVisitedOrganization) {
router.replace(`/new/${lastVisitedOrganization}`, undefined, { shallow: true })
} else {
router.replace(`/new/_`, undefined, { shallow: true })
}
}
}, [slug, lastVisitedOrganization, router])
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmitWithComputeCostsConfirmation, (errors) =>
trackFunnelError(
'project_creation',
classifyValidationError('project_creation', errors),
'form'
)
)}
>
<Panel
loading={!isOrganizationsSuccess}
noMargin={isVercelIntegrationFlow}
className={cn(
!isVercelIntegrationFlow && 'border-0 shadow-none rounded-none bg-transparent'
)}
title={
!isVercelIntegrationFlow && (
<div key="panel-title">
<h3>Create a new project</h3>
<p className="text-sm text-foreground-lighter text-balance">
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.
</p>
</div>
)
}
footer={
<ProjectCreationFooter
form={form}
canCreateProject={canCreateProject}
instanceSize={instanceSize}
organizationProjects={organizationProjects}
isCreatingNewProject={isCreatingNewProject}
isSuccessNewProject={isSuccessNewProject}
hideCancelButton={isVercelIntegrationFlow}
/>
}
>
<>
{projectCreationDisabled ? (
<DisabledWarningDueToIncident title="Project creation is currently disabled" />
) : (
<div className="divide-y divide-border-border">
<OrganizationSelector
form={form}
disableOrganizationSelection={isVercelIntegrationFlow}
/>
{canCreateProject && (
<>
{!isVercelIntegrationFlow && canConfigureGitHubOnCreate && (
<Panel.Content>
<GitHubRepositoryField
form={form}
name="githubRepositoryId"
installationIdField="githubInstallationId"
repositoryNameField="githubRepositoryName"
label="GitHub (optional)"
description={
<>
Ideal for agent-first workflows: update your schema in code, push it
to GitHub, and Supabase deploys the changes automatically.{' '}
<a
href="https://supabase.com/docs/guides/deployment/branching/github-integration"
target="_blank"
rel="noreferrer noopener"
className="text-link"
>
Learn more
</a>
</>
}
disabled={isCreatingNewProject}
repositories={githubRepos}
gitHubAuthorization={gitHubAuthorization}
hasPartialResponseDueToSSO={hasPartialResponseDueToSSO}
isLoading={isLoadingRepositoryOptions}
refetch={refetchRepositoryOptions}
onConnectClick={() => track('project_creation_github_connect_clicked')}
/>
</Panel.Content>
)}
<ProjectNameInput form={form} />
{canChooseInstanceSize && <ComputeSizeSelector form={form} />}
<DatabasePasswordInput form={form} />
<RegionSelector
form={form}
instanceSize={instanceSize as DesiredInstanceSize}
/>
{isVercelIntegrationFlow && !!externalId && <DataSeeding form={form} />}
<SecurityOptions form={form} surface={surface} />
{showInternalOnlyConfiguration && <InternalOnlyConfiguration form={form} />}
{showAdvancedConfig && !!availableOrioleVersion && (
<AdvancedConfiguration form={form} />
)}
{shouldShowFreeProjectInfo ? (
<Admonition
className="rounded-none border-0"
type="note"
title="Need a free project?"
description={
<p>
You can have up to 2 free projects across all organizations.{' '}
<Link className="underline text-foreground" href="/new">
Create a free organization
</Link>{' '}
to use them.
</p>
}
/>
) : null}
</>
)}
{freePlanWithExceedingLimits ? (
isAdmin &&
slug && (
<FreeProjectLimitWarning membersExceededLimit={membersExceededLimit || []} />
)
) : hasOutstandingInvoices ? (
<Panel.Content>
<Admonition
type="default"
title="Your organization has overdue invoices"
description={
<div className="space-y-3">
<p className="text-sm leading-normal">
Please resolve all outstanding invoices first before creating a new
project
</p>
<div>
<Button asChild variant="default">
<Link href={`/org/${slug}/billing#invoices`}>View invoices</Link>
</Button>
</div>
</div>
}
/>
</Panel.Content>
) : null}
</div>
)}
</>
</Panel>
<ConfirmationModal
size="large"
variant="warning"
loading={isCreatingNewProject}
visible={isComputeCostsConfirmationModalVisible}
title="Confirm compute costs"
confirmLabel="I understand"
onCancel={() => setIsComputeCostsConfirmationModalVisible(false)}
onConfirm={async () => {
const values = form.getValues()
await onSubmit(values)
setIsComputeCostsConfirmationModalVisible(false)
}}
>
<div className="text-sm text-foreground-light space-y-1">
<p>
Launching a project on compute size "{instanceLabel(instanceSize)}" increases your
monthly costs by ${additionalMonthlySpend}, independent of how actively you use it. By
clicking "I understand", you agree to the additional costs.{' '}
<Link
href={`${DOCS_URL}/guides/platform/manage-your-usage/compute`}
target="_blank"
className="underline"
>
Compute Costs
</Link>{' '}
are non-refundable.
</p>
</div>
</ConfirmationModal>
</form>
</Form>
)
}