diff --git a/apps/studio/components/interfaces/Realtime/Inspector/useRealtimeMessages.ts b/apps/studio/components/interfaces/Realtime/Inspector/useRealtimeMessages.ts
index adfff8d9a1e9b..40643867f1b20 100644
--- a/apps/studio/components/interfaces/Realtime/Inspector/useRealtimeMessages.ts
+++ b/apps/studio/components/interfaces/Realtime/Inspector/useRealtimeMessages.ts
@@ -168,7 +168,7 @@ export const useRealtimeMessages = (
}
// Finally, subscribe to the Channel we just setup
- newChannel.subscribe(async (status) => {
+ newChannel.subscribe(async (status, err) => {
if (status === 'SUBSCRIBED') {
// Let LiveView know we connected so we can update the button text
// pushMessageTo('#conn_info', 'broadcast_subscribed', { host: host })
@@ -192,9 +192,11 @@ export const useRealtimeMessages = (
})
}
} else if (status === 'CHANNEL_ERROR') {
- toast.error(
- `Failed to connect to the channel ${channelName}: This may be due to restrictive RLS policies. Check your role and try again.`
- )
+ if (err?.message) {
+ toast.error(`Failed to connect with the following error: ${err.message}`)
+ } else {
+ toast.error(`Failed to connect. Please check your RLS policies and try again.`)
+ }
newChannel.unsubscribe()
setChannel(undefined)
diff --git a/apps/studio/data/organizations/keys.ts b/apps/studio/data/organizations/keys.ts
index a20248ab4052b..64dabc7a7f67a 100644
--- a/apps/studio/data/organizations/keys.ts
+++ b/apps/studio/data/organizations/keys.ts
@@ -1,3 +1,5 @@
+import type { DesiredInstanceSizeForAvailableRegions } from './organization-available-regions-query'
+
export const organizationKeys = {
list: () => ['organizations'] as const,
detail: (slug?: string) => ['organizations', slug] as const,
@@ -20,6 +22,9 @@ export const organizationKeys = {
['organizations', slug, 'validate-token', token] as const,
projectClaim: (slug: string, token: string) =>
['organizations', slug, 'project-claim', token] as const,
- availableRegions: (slug: string | undefined, cloudProvider: string) =>
- ['organizations', slug, 'available-regions', cloudProvider] as const,
+ availableRegions: (
+ slug: string | undefined,
+ cloudProvider: string,
+ size?: DesiredInstanceSizeForAvailableRegions
+ ) => ['organizations', slug, 'available-regions', cloudProvider, size] as const,
}
diff --git a/apps/studio/data/organizations/organization-available-regions-query.ts b/apps/studio/data/organizations/organization-available-regions-query.ts
index 01bcd9af28f14..7d037845ae965 100644
--- a/apps/studio/data/organizations/organization-available-regions-query.ts
+++ b/apps/studio/data/organizations/organization-available-regions-query.ts
@@ -1,16 +1,21 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
+import type { operations } from 'api-types'
import { get, handleError } from 'data/fetchers'
import type { ResponseError } from 'types'
import { organizationKeys } from './keys'
+export type DesiredInstanceSizeForAvailableRegions =
+ operations['v1-get-available-regions']['parameters']['query']['desired_instance_size']
+
export type OrganizationAvailableRegionsVariables = {
slug?: string
cloudProvider: 'AWS' | 'FLY' | 'AWS_K8S' | 'AWS_NIMBUS'
+ desiredInstanceSize?: DesiredInstanceSizeForAvailableRegions
}
export async function getOrganizationAvailableRegions(
- { slug, cloudProvider }: OrganizationAvailableRegionsVariables,
+ { slug, cloudProvider, desiredInstanceSize }: OrganizationAvailableRegionsVariables,
signal?: AbortSignal
) {
if (!slug) throw new Error('slug is required')
@@ -20,6 +25,7 @@ export async function getOrganizationAvailableRegions(
query: {
cloud_provider: cloudProvider,
organization_slug: slug,
+ desired_instance_size: desiredInstanceSize,
},
},
signal,
@@ -34,7 +40,7 @@ export type OrganizationAvailableRegionsData = Awaited<
export type OrganizationAvailableRegionsError = ResponseError
export const useOrganizationAvailableRegionsQuery =
(
- { slug, cloudProvider }: OrganizationAvailableRegionsVariables,
+ { slug, cloudProvider, desiredInstanceSize }: OrganizationAvailableRegionsVariables,
{
enabled = true,
...options
@@ -44,8 +50,10 @@ export const useOrganizationAvailableRegionsQuery = = {}
) =>
- useQuery(
- organizationKeys.availableRegions(slug, cloudProvider),
- ({ signal }) => getOrganizationAvailableRegions({ slug, cloudProvider }, signal),
- { enabled: enabled && typeof slug !== 'undefined', ...options }
- )
+ useQuery({
+ queryKey: organizationKeys.availableRegions(slug, cloudProvider, desiredInstanceSize),
+ queryFn: ({ signal }) =>
+ getOrganizationAvailableRegions({ slug, cloudProvider, desiredInstanceSize }, signal),
+ enabled: enabled && typeof slug !== 'undefined',
+ ...options,
+ })
diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx
index 76f66587a5868..9d4dbad820c3d 100644
--- a/apps/studio/pages/new/[slug].tsx
+++ b/apps/studio/pages/new/[slug].tsx
@@ -1,7 +1,6 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { debounce } from 'lodash'
-import { ExternalLink } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { PropsWithChildren, useEffect, useMemo, useRef, useState } from 'react'
@@ -121,7 +120,7 @@ const FormSchema = z.object({
dbPass: z
.string({ required_error: 'Please enter a database password.' })
.min(1, 'Password is required.'),
- instanceSize: z.string(),
+ instanceSize: z.string().optional(),
dataApi: z.boolean(),
useApiSchema: z.boolean(),
postgresVersionSelection: z.string(),
@@ -133,17 +132,29 @@ export type CreateProjectForm = z.infer
const Wizard: NextPageWithLayout = () => {
const router = useRouter()
const { slug, projectName } = useParams()
+ const defaultProvider = useDefaultProvider()
+
const { data: currentOrg } = useSelectedOrganizationQuery()
const isFreePlan = currentOrg?.plan?.id === 'free'
+ const canChooseInstanceSize = !isFreePlan
+
const [lastVisitedOrganization] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION,
''
)
+ const { can: isAdmin } = useAsyncCheckPermissions(PermissionAction.CREATE, 'projects')
- const showAdvancedConfig = useIsFeatureEnabled('project_creation:show_advanced_config')
+ const smartRegionEnabled = useFlag('enableSmartRegion')
+ const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate')
+ const showPostgresVersionSelector = useFlag('showPostgresVersionSelector')
+ const cloudProviderEnabled = useFlag('enableFlyCloudProvider')
+ const showAdvancedConfig = useIsFeatureEnabled('project_creation:show_advanced_config')
const { infraCloudProviders: validCloudProviders } = useCustomContent(['infra:cloud_providers'])
+ const showNonProdFields = process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod'
+ const isManagedByVercel = currentOrg?.managed_by === 'vercel-marketplace'
+
// 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) {
@@ -153,33 +164,69 @@ const Wizard: NextPageWithLayout = () => {
}
}
+ const [allProjects, setAllProjects] = useState(undefined)
+ const [passwordStrengthMessage, setPasswordStrengthMessage] = useState('')
+ const [passwordStrengthWarning, setPasswordStrengthWarning] = useState('')
+ const [isComputeCostsConfirmationModalVisible, setIsComputeCostsConfirmationModalVisible] =
+ useState(false)
+
const { mutate: sendEvent } = useSendEventMutation()
- const smartRegionEnabled = useFlag('enableSmartRegion')
- const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate')
- const showPostgresVersionSelector = useFlag('showPostgresVersionSelector')
- const cloudProviderEnabled = useFlag('enableFlyCloudProvider')
+ FormSchema.superRefine(({ dbPassStrength }, refinementContext) => {
+ if (dbPassStrength < DEFAULT_MINIMUM_PASSWORD_STRENGTH) {
+ refinementContext.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: ['dbPass'],
+ message: passwordStrengthWarning || 'Password not secure enough',
+ })
+ }
+ })
+
+ const form = useForm>({
+ resolver: zodResolver(FormSchema),
+ mode: 'onChange',
+ defaultValues: {
+ organization: slug,
+ projectName: projectName || '',
+ postgresVersion: '',
+ cloudProvider: PROVIDERS[defaultProvider].id,
+ dbPass: '',
+ dbPassStrength: 0,
+ dbRegion: undefined,
+ instanceSize: canChooseInstanceSize ? sizes[0] : undefined,
+ dataApi: true,
+ useApiSchema: false,
+ postgresVersionSelection: '',
+ useOrioleDb: false,
+ },
+ })
+ const { instanceSize: watchedInstanceSize, cloudProvider, dbRegion, organization } = form.watch()
+
+ // [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 isInvalidSlug = isOrganizationsSuccess && currentOrg === undefined
+ const orgNotFound = isOrganizationsSuccess && (organizations?.length ?? 0) > 0 && isInvalidSlug
+ const isEmptyOrganizations = (organizations?.length ?? 0) <= 0 && isOrganizationsSuccess
const { data: approvedOAuthApps } = useAuthorizedAppsQuery(
{ slug },
{ enabled: !isFreePlan && slug !== '_' }
)
-
const hasOAuthApps = approvedOAuthApps && approvedOAuthApps.length > 0
- const [passwordStrengthMessage, setPasswordStrengthMessage] = useState('')
- const [passwordStrengthWarning, setPasswordStrengthWarning] = useState('')
-
- const [isComputeCostsConfirmationModalVisible, setIsComputeCostsConfirmationModalVisible] =
- useState(false)
-
- const { data: organizations, isSuccess: isOrganizationsSuccess } = useOrganizationsQuery()
-
const isNotOnTeamOrEnterprisePlan = useMemo(
() => !['team', 'enterprise'].includes(currentOrg?.plan.id ?? ''),
[currentOrg]
@@ -188,7 +235,6 @@ const Wizard: NextPageWithLayout = () => {
const { data: allOverdueInvoices } = useOverdueInvoicesQuery({
enabled: isNotOnTeamOrEnterprisePlan,
})
-
const overdueInvoices = (allOverdueInvoices ?? []).filter(
(x) => x.organization_id === currentOrg?.id
)
@@ -219,14 +265,9 @@ const Wizard: NextPageWithLayout = () => {
() => orgProjectsFromApi?.pages.flatMap((page) => page.projects),
[orgProjectsFromApi?.pages]
)
-
- const [allProjects, setAllProjects] = useState(undefined)
-
const organizationProjects =
allProjects?.filter((project) => project.status !== PROJECT_STATUS.INACTIVE) ?? []
- const defaultProvider = useDefaultProvider()
-
const { data: _defaultRegion, error: defaultRegionError } = useDefaultRegionQuery(
{
cloudProvider: PROVIDERS[defaultProvider].id,
@@ -246,6 +287,7 @@ const Wizard: NextPageWithLayout = () => {
{
slug: slug,
cloudProvider: PROVIDERS[defaultProvider].id,
+ desiredInstanceSize: instanceSize as DesiredInstanceSize,
},
{
enabled: smartRegionEnabled,
@@ -255,7 +297,6 @@ const Wizard: NextPageWithLayout = () => {
refetchOnReconnect: false,
}
)
-
const regionError =
smartRegionEnabled && defaultProvider !== 'AWS_NIMBUS'
? availableRegionsError
@@ -267,68 +308,9 @@ const Wizard: NextPageWithLayout = () => {
? availableRegionsData?.recommendations.smartGroup.name
: _defaultRegion
- const { can: isAdmin } = useAsyncCheckPermissions(PermissionAction.CREATE, 'projects')
-
- const isInvalidSlug = isOrganizationsSuccess && currentOrg === undefined
- const orgNotFound = isOrganizationsSuccess && (organizations?.length ?? 0) > 0 && isInvalidSlug
- const isEmptyOrganizations = (organizations?.length ?? 0) <= 0 && isOrganizationsSuccess
-
- const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0
-
- const showNonProdFields = process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod'
-
- const freePlanWithExceedingLimits = isFreePlan && hasMembersExceedingFreeTierLimit
-
- const isManagedByVercel = currentOrg?.managed_by === 'vercel-marketplace'
-
const canCreateProject =
isAdmin && !freePlanWithExceedingLimits && !isManagedByVercel && !hasOutstandingInvoices
- const delayedCheckPasswordStrength = useRef(
- debounce((value) => checkPasswordStrength(value), 300)
- ).current
-
- async function checkPasswordStrength(value: any) {
- const { message, warning, strength } = await passwordStrength(value)
-
- form.setValue('dbPassStrength', strength)
- form.trigger('dbPassStrength')
- form.trigger('dbPass')
-
- setPasswordStrengthWarning(warning)
- setPasswordStrengthMessage(message)
- }
-
- FormSchema.superRefine(({ dbPassStrength }, refinementContext) => {
- if (dbPassStrength < DEFAULT_MINIMUM_PASSWORD_STRENGTH) {
- refinementContext.addIssue({
- code: z.ZodIssueCode.custom,
- path: ['dbPass'],
- message: passwordStrengthWarning || 'Password not secure enough',
- })
- }
- })
-
- const form = useForm>({
- resolver: zodResolver(FormSchema),
- mode: 'onChange',
- defaultValues: {
- organization: slug,
- projectName: projectName || '',
- postgresVersion: '',
- cloudProvider: PROVIDERS[defaultProvider].id,
- dbPass: '',
- dbPassStrength: 0,
- dbRegion: defaultRegion || undefined,
- instanceSize: sizes[0],
- dataApi: true,
- useApiSchema: false,
- postgresVersionSelection: '',
- useOrioleDb: false,
- },
- })
-
- const { instanceSize, cloudProvider, dbRegion, organization } = form.watch()
const dbRegionExact = smartRegionToExactRegion(dbRegion)
const availableOrioleVersion = useAvailableOrioleImageVersion(
@@ -359,6 +341,20 @@ const Wizard: NextPageWithLayout = () => {
? 0
: instanceSizeSpecs[instanceSize as DesiredInstanceSize]!.priceMonthly - availableComputeCredits
+ async function checkPasswordStrength(value: any) {
+ const { message, warning, strength } = await passwordStrength(value)
+
+ form.setValue('dbPassStrength', strength)
+ form.trigger('dbPassStrength')
+ form.trigger('dbPass')
+
+ setPasswordStrengthWarning(warning)
+ setPasswordStrengthMessage(message)
+ }
+ const delayedCheckPasswordStrength = useRef(
+ debounce((value) => checkPasswordStrength(value), 300)
+ ).current
+
// [Refactor] DB Password could be a common component used in multiple pages with repeated logic
function generatePassword() {
const password = generateStrongPassword()
@@ -479,6 +475,16 @@ const Wizard: NextPageWithLayout = () => {
}
}, [regionError])
+ useEffect(() => {
+ if (watchedInstanceSize !== instanceSize) {
+ form.setValue('instanceSize', instanceSize, {
+ shouldDirty: false,
+ shouldValidate: false,
+ shouldTouch: false,
+ })
+ }
+ }, [instanceSize, watchedInstanceSize, form])
+
return (