[slug].tsx678 lines · main
1// @ts-nocheck
2import { zodResolver } from '@hookform/resolvers/zod'
3import { PermissionAction } from '@supabase/shared-types/out/constants'
4import { LOCAL_STORAGE_KEYS, useFlag, useParams } from 'common'
5import Head from 'next/head'
6import Link from 'next/link'
7import { useRouter } from 'next/router'
8import { PropsWithChildren, useEffect, useMemo, useState } from 'react'
9import { useForm } from 'react-hook-form'
10import { AWS_REGIONS, type CloudProvider } from 'shared-data'
11import { toast } from 'sonner'
12import { Button, Form, useWatch } from 'ui'
13import { Admonition } from 'ui-patterns/admonition'
14import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
15import { z } from 'zod'
16
17import { AUTO_ENABLE_RLS_EVENT_TRIGGER_SQL } from '@/components/interfaces/Database/Triggers/EventTriggersList/EventTriggers.constants'
18import { AdvancedConfiguration } from '@/components/interfaces/ProjectCreation/AdvancedConfiguration'
19import { ComputeSizeSelector } from '@/components/interfaces/ProjectCreation/ComputeSizeSelector'
20import { DatabasePasswordInput } from '@/components/interfaces/ProjectCreation/DatabasePasswordInput'
21import { DisabledWarningDueToIncident } from '@/components/interfaces/ProjectCreation/DisabledWarningDueToIncident'
22import { FreeProjectLimitWarning } from '@/components/interfaces/ProjectCreation/FreeProjectLimitWarning'
23import { InternalOnlyConfiguration } from '@/components/interfaces/ProjectCreation/InternalOnlyConfiguration'
24import { OrganizationSelector } from '@/components/interfaces/ProjectCreation/OrganizationSelector'
25import { extractPostgresVersionDetails } from '@/components/interfaces/ProjectCreation/PostgresVersionSelector'
26import { sizes } from '@/components/interfaces/ProjectCreation/ProjectCreation.constants'
27import { FormSchema } from '@/components/interfaces/ProjectCreation/ProjectCreation.schema'
28import {
29 instanceLabel,
30 smartRegionToExactRegion,
31} from '@/components/interfaces/ProjectCreation/ProjectCreation.utils'
32import { ProjectCreationFooter } from '@/components/interfaces/ProjectCreation/ProjectCreationFooter'
33import { ProjectNameInput } from '@/components/interfaces/ProjectCreation/ProjectNameInput'
34import { RegionSelector } from '@/components/interfaces/ProjectCreation/RegionSelector'
35import { SecurityOptions } from '@/components/interfaces/ProjectCreation/SecurityOptions'
36import {
37 GitHubRepositoryField,
38 useGitHubRepositoryOptions,
39} from '@/components/interfaces/Settings/Integrations/GithubIntegration/GitHubRepositoryField'
40import DefaultLayout from '@/components/layouts/DefaultLayout'
41import { WizardLayoutWithoutAuth } from '@/components/layouts/WizardLayout'
42import Panel from '@/components/ui/Panel'
43import { useAvailableOrioleImageVersion } from '@/data/config/project-creation-postgres-versions-query'
44import { useOverdueInvoicesQuery } from '@/data/invoices/invoices-overdue-query'
45import { useDefaultRegionQuery } from '@/data/misc/get-default-region-query'
46import { useAuthorizedAppsQuery } from '@/data/oauth/authorized-apps-query'
47import { useFreeProjectLimitCheckQuery } from '@/data/organizations/free-project-limit-check-query'
48import { useOrganizationAvailableRegionsQuery } from '@/data/organizations/organization-available-regions-query'
49import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
50import { DesiredInstanceSize, instanceSizeSpecs } from '@/data/projects/new-project.constants'
51import {
52 OrgProject,
53 useOrgProjectsInfiniteQuery,
54} from '@/data/projects/org-projects-infinite-query'
55import {
56 ProjectCreateVariables,
57 useProjectCreateMutation,
58} from '@/data/projects/project-create-mutation'
59import { useCustomContent } from '@/hooks/custom-content/useCustomContent'
60import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
61import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
62import { useDataApiRevokeOnCreateDefaultEnabled } from '@/hooks/misc/useDataApiRevokeOnCreateDefault'
63import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
64import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
65import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
66import { withAuth } from '@/hooks/misc/withAuth'
67import { usePHFlag } from '@/hooks/ui/useFlag'
68import { DOCS_URL, PROJECT_STATUS, PROVIDERS, useDefaultProvider } from '@/lib/constants'
69import { buildStudioPageTitle } from '@/lib/page-title'
70import { useProfile } from '@/lib/profile'
71import { useTrack } from '@/lib/telemetry/track'
72import type { NextPageWithLayout } from '@/types'
73
74const sizesWithNoCostConfirmationRequired: DesiredInstanceSize[] = ['micro', 'small']
75
76const Wizard: NextPageWithLayout = () => {
77 const track = useTrack()
78 const router = useRouter()
79 const { slug, projectName } = useParams()
80 const { appTitle } = useCustomContent(['app:title'])
81 const defaultProvider = useDefaultProvider()
82 const { profile } = useProfile()
83 const pageTitle = buildStudioPageTitle({
84 section: 'New Project',
85 brand: appTitle || 'Briven',
86 })
87
88 const { data: currentOrg } = useSelectedOrganizationQuery()
89 const isFreePlan = currentOrg?.plan?.id === 'free'
90 const canChooseInstanceSize = !isFreePlan
91
92 const [lastVisitedOrganization] = useLocalStorageQuery(
93 LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION,
94 ''
95 )
96 const { can: isAdmin } = useAsyncCheckPermissions(PermissionAction.CREATE, 'projects')
97 const { can: canCreateGitHubConnection } = useAsyncCheckPermissions(
98 PermissionAction.CREATE,
99 'integrations.github_connections'
100 )
101 const showAdvancedConfig = useIsFeatureEnabled('project_creation:show_advanced_config')
102 const { hasAccess: hasAccessToGitHubIntegration } = useCheckEntitlements(
103 'integrations.github_connections'
104 )
105
106 const smartRegionEnabled = useFlag('enableSmartRegion')
107 const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate')
108 const showInternalOnlyConfiguration = useFlag('newProjectInternalOnlyConfiguration')
109
110 // Read the raw flag for telemetry — coerce-undefined-to-false would record false for
111 // users whose flags haven't loaded yet. The raw value preserves undefined (omitted from
112 // PostHog) so we only record true/false when the flag is resolved.
113 const dataApiRevokeOnCreateDefaultFlag = usePHFlag<boolean>('dataApiRevokeOnCreateDefault')
114 const isDataApiRevokeOnCreateDefault = useDataApiRevokeOnCreateDefaultEnabled()
115
116 const isNotOnHigherPlan = !['team', 'enterprise', 'platform'].includes(currentOrg?.plan.id ?? '')
117
118 // 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
119 if (slug === 'last-visited-org') {
120 if (lastVisitedOrganization) {
121 router.replace(`/new/${lastVisitedOrganization}`, undefined, { shallow: true })
122 } else {
123 router.replace(`/new/_`, undefined, { shallow: true })
124 }
125 }
126
127 const [allProjects, setAllProjects] = useState<OrgProject[] | undefined>(undefined)
128 const [isComputeCostsConfirmationModalVisible, setIsComputeCostsConfirmationModalVisible] =
129 useState(false)
130
131 const form = useForm<z.infer<typeof FormSchema>>({
132 resolver: zodResolver(FormSchema as any),
133 mode: 'onChange',
134 defaultValues: {
135 organization: slug,
136 projectName: projectName || '',
137 highAvailability: false,
138 postgresVersion: '',
139 instanceType: '',
140 cloudProvider: PROVIDERS[defaultProvider].id,
141 dbPass: '',
142 dbPassStrength: 0,
143 dbPassStrengthMessage: '',
144 dbRegion: undefined,
145 githubRepositoryId: '',
146 githubInstallationId: undefined,
147 githubRepositoryName: '',
148 instanceSize: canChooseInstanceSize ? sizes[0] : undefined,
149 dataApi: true,
150 dataApiDefaultPrivileges: !isDataApiRevokeOnCreateDefault,
151 enableRlsEventTrigger: false,
152 postgresVersionSelection: '',
153 useOrioleDb: false,
154 },
155 })
156 const { getFieldState, resetField, setValue } = form
157 const {
158 instanceSize: watchedInstanceSize,
159 cloudProvider,
160 dbRegion,
161 githubRepositoryName,
162 organization,
163 projectName: watchedProjectName,
164 highAvailability,
165 } = useWatch({ control: form.control })
166
167 // Read dirty state during render rather than depending on form.formState in the
168 // effect — form.formState is a Proxy that gets a new reference every render, which
169 // would re-fire this effect after each setValue and trigger an infinite loop.
170 const isDataApiDefaultPrivilegesDirty = getFieldState(
171 'dataApiDefaultPrivileges',
172 form.formState
173 ).isDirty
174
175 useEffect(() => {
176 if (dataApiRevokeOnCreateDefaultFlag === undefined) return
177 if (isDataApiDefaultPrivilegesDirty) return
178 setValue('dataApiDefaultPrivileges', !dataApiRevokeOnCreateDefaultFlag, {
179 shouldDirty: false,
180 })
181 }, [dataApiRevokeOnCreateDefaultFlag, isDataApiDefaultPrivilegesDirty, setValue])
182
183 // [Charis] Since the form is updated in a useEffect, there is an edge case
184 // when switching from free to paid, where canChooseInstanceSize is true for
185 // an in-between render, but watchedInstanceSize is still undefined from the
186 // form state carried over from the free plan. To avoid this, we set a
187 // default instance size in this case.
188 const instanceSize = canChooseInstanceSize ? (watchedInstanceSize ?? sizes[0]) : undefined
189 const { data: membersExceededLimit = [] } = useFreeProjectLimitCheckQuery(
190 { slug },
191 { enabled: isFreePlan }
192 )
193 const hasMembersExceedingFreeTierLimit = membersExceededLimit.length > 0
194 const freePlanWithExceedingLimits = isFreePlan && hasMembersExceedingFreeTierLimit
195
196 const { data: organizations = [], isSuccess: isOrganizationsSuccess } = useOrganizationsQuery()
197 const isEmptyOrganizations = isOrganizationsSuccess && organizations.length <= 0
198
199 const { data: approvedOAuthApps = [] } = useAuthorizedAppsQuery(
200 { slug },
201 { enabled: !isFreePlan && slug !== '_' }
202 )
203 const hasOAuthApps = approvedOAuthApps.length > 0
204
205 const { data: allOverdueInvoices = [] } = useOverdueInvoicesQuery({
206 enabled: isNotOnHigherPlan,
207 })
208 const overdueInvoices = allOverdueInvoices.filter((x) => x.organization_id === currentOrg?.id)
209 const hasOutstandingInvoices = isNotOnHigherPlan && overdueInvoices.length > 0
210
211 const { data: orgProjectsFromApi } = useOrgProjectsInfiniteQuery({ slug: currentOrg?.slug })
212 const allOrgProjects = useMemo(
213 () => orgProjectsFromApi?.pages.flatMap((page) => page.projects),
214 [orgProjectsFromApi?.pages]
215 )
216 const organizationProjects =
217 allProjects?.filter((project) => project.status !== PROJECT_STATUS.INACTIVE) ?? []
218 const availableComputeCredits = organizationProjects.length === 0 ? 10 : 0
219 const additionalMonthlySpend = isFreePlan
220 ? 0
221 : instanceSizeSpecs[instanceSize as DesiredInstanceSize]!.priceMonthly - availableComputeCredits
222
223 const { data: _defaultRegion, error: defaultRegionError } = useDefaultRegionQuery(
224 {
225 cloudProvider: PROVIDERS[defaultProvider].id,
226 },
227 {
228 enabled: !smartRegionEnabled,
229 refetchOnMount: false,
230 refetchOnWindowFocus: false,
231 refetchInterval: false,
232 refetchOnReconnect: false,
233 retry: false,
234 }
235 )
236
237 const { data: availableRegionsData, error: availableRegionsError } =
238 useOrganizationAvailableRegionsQuery(
239 {
240 slug: slug,
241 cloudProvider: PROVIDERS[cloudProvider as CloudProvider].id,
242 desiredInstanceSize: instanceSize as DesiredInstanceSize,
243 },
244 {
245 enabled: smartRegionEnabled,
246 refetchOnMount: false,
247 refetchOnWindowFocus: false,
248 refetchInterval: false,
249 refetchOnReconnect: false,
250 }
251 )
252 const recommendedSmartRegion = smartRegionEnabled
253 ? availableRegionsData?.recommendations.smartGroup.name
254 : ''
255
256 const regionError =
257 smartRegionEnabled && defaultProvider !== 'AWS_NIMBUS'
258 ? availableRegionsError
259 : defaultRegionError
260 const defaultRegion =
261 defaultProvider === 'AWS_NIMBUS'
262 ? AWS_REGIONS.EAST_US.displayName
263 : smartRegionEnabled
264 ? availableRegionsData?.recommendations.smartGroup.name
265 : _defaultRegion
266
267 const canCreateProject = isAdmin && !freePlanWithExceedingLimits && !hasOutstandingInvoices
268 const canConfigureGitHubOnCreate =
269 canCreateProject && hasAccessToGitHubIntegration && canCreateGitHubConnection
270
271 const dbRegionExact = smartRegionToExactRegion(dbRegion ?? '')
272
273 const availableOrioleVersion = useAvailableOrioleImageVersion(
274 {
275 cloudProvider: cloudProvider as CloudProvider,
276 dbRegion: smartRegionEnabled ? dbRegionExact : (dbRegion ?? ''),
277 organizationSlug: organization,
278 },
279 { enabled: currentOrg !== null }
280 )
281
282 const userPrimaryEmail = profile?.primary_email?.toLowerCase()
283 const isUserAtFreeProjectLimit = userPrimaryEmail
284 ? membersExceededLimit.some(
285 (member) => member.primary_email?.toLowerCase() === userPrimaryEmail
286 )
287 : false
288 const shouldShowFreeProjectInfo = !!currentOrg && !isFreePlan && !isUserAtFreeProjectLimit
289 const {
290 gitHubAuthorization,
291 githubRepos,
292 hasPartialResponseDueToSSO,
293 isLoading: isLoadingRepositoryOptions,
294 refetch: refetchRepositoryOptions,
295 } = useGitHubRepositoryOptions()
296
297 const {
298 mutate: createProject,
299 isPending: isCreatingNewProject,
300 isSuccess: isSuccessNewProject,
301 } = useProjectCreateMutation({
302 onSuccess: (res) => {
303 track(
304 'project_creation_simple_version_submitted',
305 {
306 surface: 'main',
307 instanceSize: form.getValues('instanceSize'),
308 enableRlsEventTrigger: form.getValues('enableRlsEventTrigger'),
309 dataApiEnabled: form.getValues('dataApi'),
310 dataApiDefaultPrivilegesGranted: form.getValues('dataApiDefaultPrivileges'),
311 useOrioleDb: form.getValues('useOrioleDb'),
312 ...(dataApiRevokeOnCreateDefaultFlag !== undefined && {
313 dataApiRevokeOnCreateDefaultEnabled: dataApiRevokeOnCreateDefaultFlag,
314 }),
315 },
316 {
317 project: res.ref,
318 organization: res.organization_slug,
319 }
320 )
321 router.push(`/project/${res.ref}`)
322 },
323 })
324
325 const onSubmitWithComputeCostsConfirmation = async (values: z.infer<typeof FormSchema>) => {
326 const launchingLargerInstance =
327 values.instanceSize &&
328 !sizesWithNoCostConfirmationRequired.includes(values.instanceSize as DesiredInstanceSize)
329
330 if (additionalMonthlySpend > 0 && (hasOAuthApps || launchingLargerInstance)) {
331 track('project_creation_simple_version_confirm_modal_opened', {
332 instanceSize: values.instanceSize,
333 })
334 setIsComputeCostsConfirmationModalVisible(true)
335 } else {
336 await onSubmit(values)
337 }
338 }
339
340 const onSubmit = async (values: z.infer<typeof FormSchema>) => {
341 if (!currentOrg) return console.error('Unable to retrieve current organization')
342
343 const {
344 cloudProvider,
345 projectName,
346 highAvailability,
347 dbPass,
348 dbRegion,
349 postgresVersion,
350 instanceType,
351 instanceSize,
352 dataApi,
353 dataApiDefaultPrivileges,
354 enableRlsEventTrigger,
355 postgresVersionSelection,
356 useOrioleDb,
357 githubInstallationId,
358 githubRepositoryId,
359 } = values
360
361 if (useOrioleDb && !availableOrioleVersion) {
362 return toast.error('No available OrioleDB image found, only Postgres is available')
363 }
364
365 const { postgresEngine, releaseChannel } =
366 extractPostgresVersionDetails(postgresVersionSelection)
367
368 const { smartGroup = [], specific = [] } = availableRegionsData?.all ?? {}
369 const selectedRegion = smartRegionEnabled
370 ? (smartGroup.find((x) => x.name === dbRegion) ?? specific.find((x) => x.name === dbRegion))
371 : undefined
372 const parsedGitHubRepositoryId =
373 githubRepositoryId.length > 0 ? Number(githubRepositoryId) : undefined
374 const shouldIncludeGitHubFields =
375 githubInstallationId !== undefined && Number.isFinite(parsedGitHubRepositoryId)
376
377 const data: ProjectCreateVariables = {
378 dbPass,
379 cloudProvider,
380 organizationSlug: currentOrg.slug,
381 name: projectName,
382 highAvailability,
383 // gets ignored due to org billing subscription anyway
384 dbPricingTierId: 'tier_free',
385 // only set the compute size on pro+ plans. Free plans always use micro (nano in the future) size.
386 dbInstanceSize: isFreePlan ? undefined : (instanceSize as DesiredInstanceSize),
387 dataApiExposedSchemas: !dataApi ? [] : undefined,
388 dataApiUseApiSchema: false,
389 dataApiRevokeDefaultPrivileges: dataApi && !dataApiDefaultPrivileges,
390 postgresEngine: useOrioleDb ? availableOrioleVersion?.postgres_engine : postgresEngine,
391 releaseChannel: useOrioleDb ? availableOrioleVersion?.release_channel : releaseChannel,
392 ...(smartRegionEnabled ? { regionSelection: selectedRegion } : { dbRegion }),
393 dbSql: enableRlsEventTrigger ? AUTO_ENABLE_RLS_EVENT_TRIGGER_SQL : undefined,
394 ...(shouldIncludeGitHubFields
395 ? {
396 githubInstallationId,
397 githubRepositoryId: parsedGitHubRepositoryId,
398 }
399 : {}),
400 }
401
402 if (postgresVersion && !postgresVersion.match(/1[2-9]\..*/)) {
403 toast.error(
404 `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`
405 )
406 }
407
408 if (postgresVersion || instanceType) {
409 data['customBrivenRequest'] = {
410 ami: {
411 ...(postgresVersion && {
412 search_tags: { 'tag:postgresVersion': postgresVersion },
413 }),
414 ...(instanceType && { instance_type: instanceType }),
415 },
416 }
417 }
418
419 createProject(data)
420 }
421
422 useEffect(() => {
423 // Only set once to ensure compute credits dont change while project is being created
424 if (allOrgProjects && allOrgProjects.length > 0 && !allProjects) {
425 setAllProjects(allOrgProjects)
426 }
427 }, [allOrgProjects, allProjects, setAllProjects])
428
429 useEffect(() => {
430 // Handle no org: redirect to new org route
431 if (isEmptyOrganizations) {
432 router.push(`/new`)
433 }
434 }, [isEmptyOrganizations, router])
435
436 useEffect(() => {
437 // [Joshen] Cause slug depends on router which doesnt load immediately on render
438 // While the form data does load immediately
439 if (slug && slug !== '_') setValue('organization', slug)
440 if (projectName) setValue('projectName', projectName || '')
441 }, [slug, setValue, projectName])
442
443 const isDbRegionDirty = getFieldState('dbRegion', form.formState).isDirty
444 useEffect(() => {
445 if (!isDbRegionDirty && defaultRegion) {
446 setValue('dbRegion', defaultRegion)
447 }
448 }, [defaultRegion, isDbRegionDirty, setValue])
449
450 useEffect(() => {
451 if (regionError) {
452 resetField('dbRegion', {
453 defaultValue: PROVIDERS[defaultProvider].default_region.displayName,
454 })
455 }
456 }, [regionError, resetField, defaultProvider])
457
458 useEffect(() => {
459 if (!isDbRegionDirty && recommendedSmartRegion) {
460 setValue('dbRegion', recommendedSmartRegion)
461 }
462 }, [recommendedSmartRegion, isDbRegionDirty, setValue])
463
464 useEffect(() => {
465 if (highAvailability && cloudProvider !== 'AWS_K8S') {
466 setValue('cloudProvider', 'AWS_K8S')
467 }
468 }, [highAvailability, cloudProvider, setValue])
469
470 useEffect(() => {
471 if (watchedInstanceSize !== instanceSize) {
472 setValue('instanceSize', instanceSize, {
473 shouldDirty: false,
474 shouldValidate: false,
475 shouldTouch: false,
476 })
477 }
478 }, [instanceSize, watchedInstanceSize, setValue])
479
480 useEffect(() => {
481 if (!githubRepositoryName) return
482 if ((watchedProjectName ?? '').trim().length > 0) return
483
484 const repoName = githubRepositoryName.split('/').at(-1) ?? githubRepositoryName
485 setValue('projectName', repoName.trim(), {
486 shouldValidate: true,
487 })
488 }, [githubRepositoryName, watchedProjectName, setValue])
489
490 return (
491 <>
492 {/* Wizard layouts set the visual header but not the browser tab title. */}
493 <Head>
494 <title>{pageTitle}</title>
495 <meta name="description" content="Briven Studio" />
496 </Head>
497 <Form {...form}>
498 <form onSubmit={form.handleSubmit(onSubmitWithComputeCostsConfirmation)}>
499 <Panel
500 loading={!isOrganizationsSuccess}
501 title={
502 <div key="panel-title">
503 <h3>Create a new project</h3>
504 <p className="text-sm text-foreground-lighter text-balance">
505 Your project will have its own dedicated instance and full Postgres database. An
506 API will be set up so you can easily interact with your new database.
507 </p>
508 </div>
509 }
510 footer={
511 <ProjectCreationFooter
512 form={form}
513 canCreateProject={canCreateProject}
514 instanceSize={instanceSize}
515 organizationProjects={organizationProjects}
516 isCreatingNewProject={isCreatingNewProject}
517 isSuccessNewProject={isSuccessNewProject}
518 />
519 }
520 >
521 <>
522 {projectCreationDisabled ? (
523 <DisabledWarningDueToIncident title="Project creation is currently disabled" />
524 ) : (
525 <div className="divide-y divide-border-border">
526 <OrganizationSelector form={form} />
527
528 {canCreateProject && (
529 <>
530 {canConfigureGitHubOnCreate && (
531 <Panel.Content>
532 <GitHubRepositoryField
533 form={form}
534 name="githubRepositoryId"
535 installationIdField="githubInstallationId"
536 repositoryNameField="githubRepositoryName"
537 label="GitHub (optional)"
538 description={
539 <>
540 Ideal for agent-first workflows: update your schema in code, push it
541 to GitHub, and Briven deploys the changes automatically.{' '}
542 <a
543 href="https://supabase.com/docs/guides/deployment/branching/github-integration"
544 target="_blank"
545 rel="noreferrer noopener"
546 className="text-link"
547 >
548 Learn more
549 </a>
550 </>
551 }
552 disabled={isCreatingNewProject}
553 repositories={githubRepos}
554 gitHubAuthorization={gitHubAuthorization}
555 hasPartialResponseDueToSSO={hasPartialResponseDueToSSO}
556 isLoading={isLoadingRepositoryOptions}
557 refetch={refetchRepositoryOptions}
558 onConnectClick={() => track('project_creation_github_connect_clicked')}
559 />
560 </Panel.Content>
561 )}
562 <ProjectNameInput form={form} />
563
564 {canChooseInstanceSize && <ComputeSizeSelector form={form} />}
565
566 <DatabasePasswordInput form={form} />
567
568 <RegionSelector
569 form={form}
570 instanceSize={instanceSize as DesiredInstanceSize}
571 />
572
573 <SecurityOptions form={form} />
574
575 {showInternalOnlyConfiguration && <InternalOnlyConfiguration form={form} />}
576
577 {showAdvancedConfig && !!availableOrioleVersion && (
578 <AdvancedConfiguration form={form} />
579 )}
580
581 {shouldShowFreeProjectInfo ? (
582 <Admonition
583 className="rounded-none border-0"
584 type="note"
585 title="Need a free project?"
586 description={
587 <p>
588 You can have up to 2 free projects across all organizations.{' '}
589 <Link className="underline text-foreground" href="/new">
590 Create a free organization
591 </Link>{' '}
592 to use them.
593 </p>
594 }
595 />
596 ) : null}
597 </>
598 )}
599
600 {freePlanWithExceedingLimits ? (
601 isAdmin &&
602 slug && (
603 <FreeProjectLimitWarning membersExceededLimit={membersExceededLimit || []} />
604 )
605 ) : hasOutstandingInvoices ? (
606 <Panel.Content>
607 <Admonition
608 type="default"
609 title="Your organization has overdue invoices"
610 description={
611 <div className="space-y-3">
612 <p className="text-sm leading-normal">
613 Please resolve all outstanding invoices first before creating a new
614 project
615 </p>
616
617 <div>
618 <Button asChild type="default">
619 <Link href={`/org/${slug}/billing#invoices`}>View invoices</Link>
620 </Button>
621 </div>
622 </div>
623 }
624 />
625 </Panel.Content>
626 ) : null}
627 </div>
628 )}
629 </>
630 </Panel>
631
632 <ConfirmationModal
633 size="large"
634 loading={false}
635 visible={isComputeCostsConfirmationModalVisible}
636 title="Confirm compute costs"
637 confirmLabel="I understand"
638 onCancel={() => setIsComputeCostsConfirmationModalVisible(false)}
639 onConfirm={async () => {
640 const values = form.getValues()
641 await onSubmit(values)
642 setIsComputeCostsConfirmationModalVisible(false)
643 }}
644 variant={'warning'}
645 >
646 <div className="text-sm text-foreground-light space-y-1">
647 <p>
648 Launching a project on compute size "{instanceLabel(instanceSize)}" increases your
649 monthly costs by ${additionalMonthlySpend}, independent of how actively you use it.
650 By clicking "I understand", you agree to the additional costs.{' '}
651 <Link
652 href={`${DOCS_URL}/guides/platform/manage-your-usage/compute`}
653 target="_blank"
654 className="underline"
655 >
656 Compute Costs
657 </Link>{' '}
658 are non-refundable.
659 </p>
660 </div>
661 </ConfirmationModal>
662 </form>
663 </Form>
664 </>
665 )
666}
667
668const PageLayout = withAuth(({ children }: PropsWithChildren) => {
669 return <WizardLayoutWithoutAuth>{children}</WizardLayoutWithoutAuth>
670})
671
672Wizard.getLayout = (page) => (
673 <DefaultLayout hideMobileMenu headerTitle="New project">
674 <PageLayout>{page}</PageLayout>
675 </DefaultLayout>
676)
677
678export default Wizard