Merge pull request #16563 from supabase/chore/convert-requests-to-RQ-6

Convert independent POST requests to RQ P4
This commit is contained in:
Joshen Lim authored and GitHub committed 2023-08-21 14:26:47 +07:00
commit 38dff57dd4
10 files changed
+257 -188

No files matched your search

@@ -9,6 +9,7 @@ interface ProductEmptyStateProps {
infoButtonLabel?: string
infoButtonUrl?: string
onClickCta?: () => void
loading?: boolean
disabled?: boolean
disabledMessage?: string
}
@@ -21,6 +22,7 @@ const ProductEmptyState = ({
infoButtonLabel = '',
infoButtonUrl = '',
onClickCta = () => {},
loading = false,
disabled = false,
disabledMessage = '',
}: PropsWithChildren<ProductEmptyStateProps>) => {
@@ -39,7 +41,12 @@ const ProductEmptyState = ({
{ctaButtonLabel && onClickCta && (
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger asChild>
<Button type="primary" onClick={onClickCta} disabled={disabled}>
<Button
type="primary"
onClick={onClickCta}
loading={loading}
disabled={loading || disabled}
>
{ctaButtonLabel}
</Button>
</Tooltip.Trigger>
@@ -0,0 +1,45 @@
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
import { toast } from 'react-hot-toast'
import { post } from 'data/fetchers'
import { ResponseError } from 'types'
export type HooksEnableVariables = {
ref: string
}
export async function enableDatabaseWebhooks({ ref }: HooksEnableVariables) {
const { data, error } = await post('/platform/database/{ref}/hook-enable', {
params: { path: { ref } },
})
if (error) throw error
return data
}
type HooksEnableData = Awaited<ReturnType<typeof enableDatabaseWebhooks>>
export const useHooksEnableMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseMutationOptions<HooksEnableData, ResponseError, HooksEnableVariables>,
'mutationFn'
> = {}) => {
return useMutation<HooksEnableData, ResponseError, HooksEnableVariables>(
(vars) => enableDatabaseWebhooks(vars),
{
async onSuccess(data, variables, context) {
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to enable webhooks: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
}
)
}
@@ -0,0 +1,53 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { toast } from 'react-hot-toast'
import { post } from 'data/fetchers'
import { ResponseError } from 'types'
import { organizationKeys } from './keys'
export type OrganizationCreateVariables = {
name: string
kind?: string
size?: string
}
export async function createOrganization({ name, kind, size }: OrganizationCreateVariables) {
const { data, error } = await post('/platform/organizations', {
// @ts-ignore [Joshen] Generated API spec is wrong?
body: { name, kind, ...(kind == 'COMPANY' ? { size } : {}) },
})
if (error) throw error
return data
}
type OrganizationCreateData = Awaited<ReturnType<typeof createOrganization>>
export const useOrganizationCreateMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseMutationOptions<OrganizationCreateData, ResponseError, OrganizationCreateVariables>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<OrganizationCreateData, ResponseError, OrganizationCreateVariables>(
(vars) => createOrganization(vars),
{
async onSuccess(data, variables, context) {
await queryClient.invalidateQueries(organizationKeys.list())
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to create organization: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
}
)
}
@@ -12,7 +12,11 @@ export type ProjectCreateVariables = {
dbPass: string
dbRegion: string
dbSql?: string
dbPricingTierId?: string
cloudProvider?: string
configurationId?: string
authSiteUrl?: string
customSupabaseRequest?: object
}
export async function createProject({
@@ -21,18 +25,25 @@ export async function createProject({
dbPass,
dbRegion,
dbSql,
dbPricingTierId = PRICING_TIER_PRODUCT_IDS.FREE,
cloudProvider = PROVIDERS.AWS.id,
configurationId,
authSiteUrl,
customSupabaseRequest,
}: ProjectCreateVariables) {
const response = await post(`${API_URL}/projects`, {
cloud_provider: PROVIDERS.AWS.id, // hardcoded for DB instances to be under AWS
cloud_provider: cloudProvider,
org_id: organizationId,
name,
db_pass: dbPass,
db_region: dbRegion,
db_sql: dbSql,
db_pricing_tier_id: PRICING_TIER_PRODUCT_IDS.FREE,
// auth_site_url: _store.selectedVercelProjectUrl,
db_pricing_tier_id: dbPricingTierId,
auth_site_url: authSiteUrl,
vercel_configuration_id: configurationId,
...(customSupabaseRequest !== undefined && {
custom_supabase_internal_requests: customSupabaseRequest,
}),
})
if (response.error) throw response.error
return response as ProjectBase
@@ -59,7 +70,7 @@ export const useProjectCreateMutation = ({
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to create project: ${data.message}`)
toast.error(`Failed to create new project: ${data.message}`)
} else {
onError(data, variables, context)
}
-1
View File
@@ -1,7 +1,6 @@
import { getAccessToken } from 'lib/gotrue'
import { isUndefined } from 'lodash'
import { SupaResponse } from 'types/base'
import { Session } from '@supabase/gotrue-js'
export function handleError<T>(e: any, requestId: string): SupaResponse<T> {
const message = e?.message ? `An error has occurred: ${e.message}` : 'An error has occurred'
+29 -30
View File
@@ -4,7 +4,7 @@ import generator from 'generate-password'
import { debounce, isUndefined } from 'lodash'
import { observer } from 'mobx-react-lite'
import Link from 'next/link'
import Router, { useRouter } from 'next/router'
import { useRouter } from 'next/router'
import { useEffect, useRef, useState } from 'react'
import {
Alert,
@@ -32,12 +32,14 @@ import PasswordStrengthBar from 'components/ui/PasswordStrengthBar'
import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query'
import { useOrganizationPaymentMethodsQuery } from 'data/organizations/organization-payment-methods-query'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import {
ProjectCreateVariables,
useProjectCreateMutation,
} from 'data/projects/project-create-mutation'
import { useProjectsQuery } from 'data/projects/projects-query'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useCheckPermissions, useFlag, useStore, withAuth } from 'hooks'
import { post } from 'lib/common/fetch'
import {
API_URL,
AWS_REGIONS,
CloudProvider,
DEFAULT_MINIMUM_PASSWORD_STRENGTH,
@@ -70,7 +72,6 @@ const Wizard: NextPageWithLayout = () => {
const [dbPass, setDbPass] = useState('')
const [dbRegion, setDbRegion] = useState(PROVIDERS[cloudProvider].default_region)
const [dbPricingTierKey, setDbPricingTierKey] = useState(PRICING_TIER_DEFAULT_KEY)
const [newProjectedLoading, setNewProjectLoading] = useState(false)
const [passwordStrengthMessage, setPasswordStrengthMessage] = useState('')
const [passwordStrengthWarning, setPasswordStrengthWarning] = useState('')
const [passwordStrengthScore, setPasswordStrengthScore] = useState(0)
@@ -93,6 +94,12 @@ const Wizard: NextPageWithLayout = () => {
enabled: currentOrg?.subscription_id != undefined,
})
const { mutate: createProject, isLoading: isCreatingNewProject } = useProjectCreateMutation({
onSuccess: (res) => {
router.push(`/project/${res.ref}/building`)
},
})
const orgProjectCount = (allProjects || []).filter(
(proj) => proj.organization_id === currentOrg?.id
).length
@@ -186,43 +193,31 @@ const Wizard: NextPageWithLayout = () => {
}
const onClickNext = async () => {
setNewProjectLoading(true)
if (!currentOrg) return console.error('Unable to retrieve current organization')
const dbTier = dbPricingTierKey === 'PRO' && !isSpendCapEnabled ? 'PAYG' : dbPricingTierKey
const data: Record<string, any> = {
cloud_provider: cloudProvider,
org_id: currentOrg?.id,
const data: ProjectCreateVariables = {
cloudProvider,
organizationId: currentOrg.id,
name: projectName.trim(),
db_pass: dbPass,
db_region: dbRegion,
db_pricing_tier_id: (PRICING_TIER_PRODUCT_IDS as any)[dbTier],
dbPass: dbPass,
dbRegion: dbRegion,
dbPricingTierId: (PRICING_TIER_PRODUCT_IDS as any)[dbTier],
}
if (postgresVersion) {
if (!postgresVersion.match(/1[2-9]\..*/)) {
setNewProjectLoading(false)
ui.setNotification({
return ui.setNotification({
category: 'error',
message: `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`,
})
return
}
data['custom_supabase_internal_requests'] = {
data['customSupabaseRequest'] = {
ami: { search_tags: { 'tag:postgresVersion': postgresVersion } },
}
}
const response = await post(`${API_URL}/projects`, data)
if (response.error) {
setNewProjectLoading(false)
ui.setNotification({
error: response.error,
category: 'error',
message: `Failed to create new project: ${response.error.message}`,
})
} else {
router.push(`/project/${response.ref}/building`)
}
createProject(data)
}
// [Joshen] Refactor: DB Password could be a common component
@@ -263,7 +258,11 @@ const Wizard: NextPageWithLayout = () => {
}
footer={
<div key="panel-footer" className="flex items-center justify-between w-full">
<Button type="default" onClick={() => Router.push('/projects')}>
<Button
type="default"
disabled={isCreatingNewProject}
onClick={() => router.push('/projects')}
>
Cancel
</Button>
<div className="items-center space-x-3">
@@ -272,8 +271,8 @@ const Wizard: NextPageWithLayout = () => {
)}
<Button
onClick={onClickNext}
loading={newProjectedLoading}
disabled={newProjectedLoading || !canSubmit}
loading={isCreatingNewProject}
disabled={isCreatingNewProject || !canSubmit}
>
Create new project
</Button>
+10 -23
View File
@@ -1,4 +1,3 @@
import { useQueryClient } from '@tanstack/react-query'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { useState } from 'react'
@@ -6,10 +5,8 @@ import { Button, Input, Listbox } from 'ui'
import { WizardLayout } from 'components/layouts'
import Panel from 'components/ui/Panel'
import { invalidateOrganizationsQuery } from 'data/organizations/organizations-query'
import { useOrganizationCreateMutation } from 'data/organizations/organization-create-mutation'
import { useStore } from 'hooks'
import { post } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { NextPageWithLayout } from 'types'
const ORG_KIND_TYPES = {
@@ -35,14 +32,19 @@ const ORG_SIZE_DEFAULT = '1'
* No org selected yet, create a new one
*/
const Wizard: NextPageWithLayout = () => {
const queryClient = useQueryClient()
const { ui } = useStore()
const router = useRouter()
const [orgName, setOrgName] = useState('')
const [orgKind, setOrgKind] = useState(ORG_KIND_DEFAULT)
const [orgSize, setOrgSize] = useState(ORG_SIZE_DEFAULT)
const [newOrgLoading, setNewOrgLoading] = useState(false)
const { mutate: createOrganization, isLoading: newOrgLoading } = useOrganizationCreateMutation({
onSuccess: async (org: any) => {
// [Joshen] API spec is wrong? its returning org type as only having id and name
router.push(`/new/${org.slug}`)
},
})
function validateOrgName(name: any) {
return name.length >= 1
@@ -65,29 +67,14 @@ const Wizard: NextPageWithLayout = () => {
const trimmedOrgName = orgName ? orgName.trim() : ''
const isOrgNameValid = validateOrgName(trimmedOrgName)
if (!isOrgNameValid) {
ui.setNotification({ category: 'error', message: 'Organization name is empty' })
return
return ui.setNotification({ category: 'error', message: 'Organization name is empty' })
}
setNewOrgLoading(true)
const response = await post(`${API_URL}/organizations`, {
createOrganization({
name: trimmedOrgName,
kind: orgKind,
...(orgKind == 'COMPANY' ? { size: orgSize } : {}),
})
if (response.error) {
setNewOrgLoading(false)
ui.setNotification({
error: response.error,
category: 'error',
message: `Failed to create organization: ${response.error?.message ?? response.error}`,
})
} else {
const org = response
await invalidateOrganizationsQuery(queryClient)
router.push(`/new/${org.slug}`)
}
}
return (
@@ -1,25 +1,22 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import clsx from 'clsx'
import { useParams } from 'common'
import { observer } from 'mobx-react-lite'
import { useEffect, useState } from 'react'
import { IconLoader } from 'ui'
import { useParams } from 'common'
import DeleteHookModal from 'components/interfaces/Database/Hooks/DeleteHookModal'
import EditHookPanel from 'components/interfaces/Database/Hooks/EditHookPanel'
import HooksList from 'components/interfaces/Database/Hooks/HooksList/HooksList'
import { DatabaseLayout } from 'components/layouts'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import NoPermission from 'components/ui/NoPermission'
import { useHooksEnableMutation } from 'data/database/hooks-enable-mutation'
import { useCheckPermissions, useStore } from 'hooks'
import { post } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { NextPageWithLayout } from 'types'
import { IconLoader } from 'ui'
const HooksPage: NextPageWithLayout = () => {
const { meta, ui } = useStore()
const { project } = useProjectContext()
const { ref } = useParams()
const schemas = meta.schemas.list()
@@ -33,24 +30,23 @@ const HooksPage: NextPageWithLayout = () => {
const canReadWebhooks = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'triggers')
const canCreateWebhooks = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'triggers')
useEffect(() => {
if (ui.selectedProjectRef) meta.hooks.load()
}, [ui.selectedProjectRef])
const enableHooksForProject = async () => {
const res = await post(`${API_URL}/database/${ref}/hook-enable`, {})
if (res.error) {
ui.setNotification({
category: 'error',
message: `Failed to enable webhooks: ${res.error.message}`,
})
} else {
const { mutate: enableHooks, isLoading: isEnablingHooks } = useHooksEnableMutation({
onSuccess: () => {
meta.schemas.load()
ui.setNotification({
category: 'success',
message: `Successfully enabled webhooks`,
})
}
},
})
useEffect(() => {
if (ui.selectedProjectRef) meta.hooks.load()
}, [ui.selectedProjectRef])
const enableHooksForProject = async () => {
if (!ref) return console.error('Project ref is required')
enableHooks({ ref })
}
const createHook = () => {
@@ -89,8 +85,11 @@ const HooksPage: NextPageWithLayout = () => {
title="Database Webhooks"
ctaButtonLabel="Enable webhooks"
onClickCta={() => enableHooksForProject()}
disabled={!canCreateWebhooks}
disabledMessage="You need additional permissions to enable webhooks"
loading={isEnablingHooks}
disabled={isEnablingHooks || !canCreateWebhooks}
disabledMessage={
!canCreateWebhooks ? 'You need additional permissions to enable webhooks' : undefined
}
>
<p className="text-sm text-scale-1100">
Database Webhooks can be used to trigger serverless functions or send requests to an
+13 -30
View File
@@ -2,13 +2,11 @@ import { runInAction } from 'mobx'
import { observer, useLocalObservable } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { ChangeEvent, createContext, useContext, useEffect, useState } from 'react'
import { Button, Input, Select } from 'ui'
import VercelIntegrationLayout from 'components/layouts/VercelIntegrationLayout'
import { useOrganizationCreateMutation } from 'data/organizations/organization-create-mutation'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useStore } from 'hooks'
import { post } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { Button, Input, Select } from 'ui'
const PageContext = createContext(null)
function SetupOrg() {
@@ -74,38 +72,19 @@ function SetupOrg() {
export default observer(SetupOrg)
const CreateOrganization = observer(({}) => {
const PageState: any = useContext(PageContext)
const router = useRouter()
const { ui, app } = useStore()
const PageState: any = useContext(PageContext)
const { data: organizations } = useOrganizationsQuery()
const [orgName, setOrgName] = useState('')
const [loading, setLoading] = useState(false)
async function onClick() {
setLoading(true)
const response = await post(`${API_URL}/organizations`, {
name: orgName,
})
if (response.error) {
ui.setNotification({
category: 'error',
message: `Failed to set up organization: ${response.error.message}`,
})
setLoading(false)
} else {
const org = response
const { mutate: createOrganization, isLoading: isCreating } = useOrganizationCreateMutation({
onSuccess: (org) => {
PageState.selectedOrg = org
const query = new URLSearchParams(PageState.queryParams).toString()
router.push(`/vercel/setupProject?${query}`)
}
}
function onOrgNameChange(e: any) {
setOrgName(e.target.value)
}
},
})
return (
<div className="">
@@ -126,10 +105,14 @@ const CreateOrganization = observer(({}) => {
placeholder="Organization name"
descriptionText="What's the name of your company or team?"
value={orgName}
onChange={onOrgNameChange}
onChange={(e: any) => setOrgName(e.target.value)}
/>
</div>
<Button disabled={loading || orgName == ''} loading={loading} onClick={onClick}>
<Button
disabled={isCreating || orgName === ''}
loading={isCreating}
onClick={() => createOrganization({ name: orgName })}
>
Create organization
</Button>
</div>
+64 -78
View File
@@ -1,31 +1,30 @@
import { toast } from 'react-hot-toast'
import { createContext, useEffect, useContext, useRef, useState, ChangeEvent } from 'react'
import { useRouter } from 'next/router'
import { observer, useLocalObservable } from 'mobx-react-lite'
import { makeAutoObservable } from 'mobx'
import { debounce } from 'lodash'
import { Button, Input, Listbox } from 'ui'
import { Dictionary } from 'components/grid'
import generator from 'generate-password'
import { useStore } from 'hooks'
import { post } from 'lib/common/fetch'
import { passwordStrength } from 'lib/helpers'
import {
PROVIDERS,
DEFAULT_MINIMUM_PASSWORD_STRENGTH,
API_URL,
PRICING_TIER_PRODUCT_IDS,
AWS_REGIONS,
} from 'lib/constants'
import { VERCEL_INTEGRATION_CONFIGS } from 'lib/vercelConfigs'
import { debounce } from 'lodash'
import { makeAutoObservable } from 'mobx'
import { observer, useLocalObservable } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { ChangeEvent, createContext, useContext, useEffect, useRef, useState } from 'react'
import { toast } from 'react-hot-toast'
import { Button, Input, Listbox } from 'ui'
import { Dictionary } from 'components/grid'
import VercelIntegrationLayout from 'components/layouts/VercelIntegrationLayout'
import {
createVercelEnv,
fetchVercelProject,
prepareVercelEvns,
} from 'components/to-be-cleaned/Integration/Vercel.utils'
import VercelIntegrationLayout from 'components/layouts/VercelIntegrationLayout'
import Loading from 'components/ui/Loading'
import PasswordStrengthBar from 'components/ui/PasswordStrengthBar'
import { useProjectCreateMutation } from 'data/projects/project-create-mutation'
import {
AWS_REGIONS,
DEFAULT_MINIMUM_PASSWORD_STRENGTH,
PRICING_TIER_PRODUCT_IDS,
PROVIDERS,
} from 'lib/constants'
import { passwordStrength } from 'lib/helpers'
import { VERCEL_INTEGRATION_CONFIGS } from 'lib/vercelConfigs'
interface ISetupProjectStore {
token: string
@@ -144,19 +143,49 @@ const Connecting = () => (
)
const CreateProject = observer(() => {
const _store = useContext(PageContext)
const router = useRouter()
const { ui } = useStore()
const _store = useContext(PageContext)
const [projectName, setProjectName] = useState('')
const [dbPass, setDbPass] = useState('')
const [passwordStrengthMessage, setPasswordStrengthMessage] = useState('')
const [passwordStrengthScore, setPasswordStrengthScore] = useState(-1)
const [dbRegion, setDbRegion] = useState(PROVIDERS.AWS.default_region)
const [loading, setLoading] = useState(false)
const delayedCheckPasswordStrength = useRef(
debounce((value: string) => checkPasswordStrength(value), 300)
).current
const { mutate: createProject, isLoading } = useProjectCreateMutation({
onSuccess: async (res) => {
const project = { ...res, db_host: `db.${res.ref}.supabase.co`, db_password: dbPass }
_store.supabaseProjectRef = project.ref
const requiredEnvs =
VERCEL_INTEGRATION_CONFIGS.find((x) => x.id == _store.externalId)?.envs || []
const envs = prepareVercelEvns(requiredEnvs, project as any)
await Promise.allSettled(
envs.map(async (env: any) => {
try {
const data = await createVercelEnv({
...env,
vercelProjectId: _store.selectedVercelProject?.id,
vercelTeamId: _store.teamId,
vercelToken: _store.token,
})
return data
} catch (err) {
console.error(`Error: ${err}`)
}
})
)
const query = new URLSearchParams(_store.queryParams).toString()
router.push(`/vercel/complete?${query}`)
},
})
const canSubmit =
projectName != '' &&
passwordStrengthScore >= DEFAULT_MINIMUM_PASSWORD_STRENGTH &&
@@ -193,67 +222,24 @@ const CreateProject = observer(() => {
delayedCheckPasswordStrength(password)
}
async function createSupabaseProject(dbSql: string) {
const data = {
cloud_provider: PROVIDERS.AWS.id, // hardcoded for DB instances to be under AWS
org_id: Number(_store.supabaseOrgId),
name: projectName,
db_pass: dbPass,
db_region: dbRegion,
db_sql: dbSql || '',
db_pricing_tier_id: PRICING_TIER_PRODUCT_IDS.FREE,
auth_site_url: _store.selectedVercelProjectUrl,
vercel_configuration_id: _store.configurationId,
}
const project = await post(`${API_URL}/projects`, data)
return { ...project, db_host: `db.${project.ref}.supabase.co`, db_password: dbPass }
}
async function onCreateProject() {
setLoading(true)
try {
const requiredEnvs =
VERCEL_INTEGRATION_CONFIGS.find((x) => x.id == _store.externalId)?.envs || []
const dbSql =
VERCEL_INTEGRATION_CONFIGS.find((x) => x.id == _store.externalId)?.template?.sql || ''
const response = await createSupabaseProject(dbSql)
if (response.error) {
setLoading(false)
ui.setNotification({
category: 'error',
message: `Failed to create project: ${response.error.message}`,
})
return
}
const project = response
_store.supabaseProjectRef = project.ref
const envs = prepareVercelEvns(requiredEnvs, project)
await Promise.allSettled(
envs.map(async (env: any) => {
try {
const data = await createVercelEnv({
...env,
vercelProjectId: _store.selectedVercelProject?.id,
vercelTeamId: _store.teamId,
vercelToken: _store.token,
})
return data
} catch (err) {
console.error(`Error: ${err}`)
}
})
)
const query = new URLSearchParams(_store.queryParams).toString()
router.push(`/vercel/complete?${query}`)
createProject({
cloudProvider: PROVIDERS.AWS.id,
organizationId: Number(_store.supabaseOrgId),
name: projectName,
dbPass: dbPass,
dbRegion: dbRegion,
dbSql: dbSql || '',
dbPricingTierId: PRICING_TIER_PRODUCT_IDS.FREE,
configurationId: _store.configurationId,
authSiteUrl: _store.selectedVercelProjectUrl,
})
} catch (error) {
console.error('Error', error)
setLoading(false)
}
}
@@ -322,7 +308,7 @@ const CreateProject = observer(() => {
</Listbox>
</div>
</div>
<Button disabled={loading || !canSubmit} loading={loading} onClick={onCreateProject}>
<Button disabled={isLoading || !canSubmit} loading={isLoading} onClick={onCreateProject}>
Create project
</Button>
</div>