diff --git a/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx b/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx
index 70a5894d1ff..cd0ef0f72a7 100644
--- a/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx
+++ b/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx
@@ -32,7 +32,7 @@ function DatabaseBackupsNav({ active }: Props) {
label: (
Restore to new project{' '}
-
+
New
diff --git a/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/AdditionalMonthlySpend.tsx b/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/AdditionalMonthlySpend.tsx
new file mode 100644
index 00000000000..d78b34b0846
--- /dev/null
+++ b/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/AdditionalMonthlySpend.tsx
@@ -0,0 +1,45 @@
+import { instanceSizeSpecs } from 'data/projects/new-project.constants'
+import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
+
+export const AdditionalMonthlySpend = () => {
+ const { project } = useProjectContext()
+
+ /**
+ * New project will have the same compute size and disk size as the original project
+ */
+ function getAdditionalMonthlySpend() {
+ const currentProjectComputeSize = project?.infra_compute_size
+ if (!currentProjectComputeSize) {
+ return null
+ }
+
+ if (currentProjectComputeSize === 'nano') {
+ return null
+ }
+
+ const additionalMonthlySpend = instanceSizeSpecs[currentProjectComputeSize]
+
+ return additionalMonthlySpend
+ }
+
+ const additionalMonthlySpend = getAdditionalMonthlySpend()
+ if (!additionalMonthlySpend) {
+ return null
+ }
+
+ return (
+
+
+ The new project will have the same compute size and disk size as this project. You will be
+ able to update the compute size and disk size after the new project is created in{' '}
+
+ Project Settings > Compute and Disk
+
+
+
+
Additional Monthly Compute + Disk Cost
+
${additionalMonthlySpend.priceMonthly}
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/BackupsList.tsx b/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/BackupsList.tsx
new file mode 100644
index 00000000000..ad330001843
--- /dev/null
+++ b/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/BackupsList.tsx
@@ -0,0 +1,59 @@
+import Panel from 'components/ui/Panel'
+import BackupsEmpty from '../BackupsEmpty'
+import { useCloneBackupsQuery } from 'data/projects/clone-query'
+import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
+import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
+import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
+import { Badge, Button } from 'ui'
+import { TimestampInfo } from 'ui-patterns'
+
+interface BackupsListProps {
+ onSelectRestore: (id: number) => void
+}
+
+export const BackupsList = ({ onSelectRestore }: BackupsListProps) => {
+ const { project } = useProjectContext()
+ const organization = useSelectedOrganization()
+
+ const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug })
+ const isFreePlan = subscription?.plan?.id === 'free'
+
+ const { data: cloneBackups } = useCloneBackupsQuery(
+ { projectRef: project?.ref },
+ { enabled: !isFreePlan }
+ )
+
+ return (
+
+ {cloneBackups?.backups.length === 0 ? (
+ <>
+
+ >
+ ) : (
+
+ {/*
{JSON.stringify({ cloneStatus }, null, 2)} */}
+ {cloneBackups?.backups.map((backup) => {
+ if (!backup.isPhysicalBackup) return null
+ return (
+
+
+
+
+
{JSON.stringify(backup.status).replaceAll('"', '')}
+ {(backup.status as any) === 'COMPLETED' && (
+
onSelectRestore(backup.id)}
+ >
+ Restore
+
+ )}
+
+ )
+ })}
+
+ )}
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/ConfirmRestoreDialog.tsx b/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/ConfirmRestoreDialog.tsx
new file mode 100644
index 00000000000..64a63c5b5b5
--- /dev/null
+++ b/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/ConfirmRestoreDialog.tsx
@@ -0,0 +1,84 @@
+import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
+import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
+import {
+ Button,
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogSection,
+ DialogTitle,
+} from 'ui'
+import { AdditionalMonthlySpend } from './AdditionalMonthlySpend'
+
+interface ConfirmRestoreDialogProps {
+ open: boolean
+ onOpenChange: (value: boolean) => void
+ onSelectContinue: () => void
+}
+
+export const ConfirmRestoreDialog = ({
+ open,
+ onOpenChange,
+ onSelectContinue,
+}: ConfirmRestoreDialogProps) => {
+ const { project } = useProjectContext()
+ const organization = useSelectedOrganization()
+
+ return (
+
+
+
+ Confirm restore to a new project
+
+ This process will create a new project and restore your database to it.
+
+
+
+
+
+ Project organization will stay the same: {organization?.name}
+
+
+ Project region will stay the same: {project?.region || ''}
+
+
+ A project can only be restored to another project once.
+
+ This is a temporary limitation. Please contact us if you need to restore a project
+ to multiple other projects.
+
+
+
+
+ What will be transferred?
+
+ Database schema (tables, views, procedures)
+ All data and indexes
+ Database roles, permissions and users
+
+
+
+ What needs manual reconfiguration?
+
+ Storage objects & settings
+ Edge Functions
+ Auth settings & API keys
+ Realtime settings
+ Database extensions and settings
+ Read replicas
+
+
+
+
+
+ onOpenChange(false)}>
+ Cancel
+
+ onSelectContinue()}>Continue
+
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/CreateNewProjectDialog.tsx b/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/CreateNewProjectDialog.tsx
new file mode 100644
index 00000000000..e795f55814b
--- /dev/null
+++ b/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/CreateNewProjectDialog.tsx
@@ -0,0 +1,205 @@
+import { zodResolver } from '@hookform/resolvers/zod'
+import { debounce } from 'lodash'
+import { useRef, useState } from 'react'
+import { useForm } from 'react-hook-form'
+import { toast } from 'sonner'
+import { z } from 'zod'
+
+import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
+import PasswordStrengthBar from 'components/ui/PasswordStrengthBar'
+import { useProjectCloneMutation } from 'data/projects/clone-mutation'
+import { useCloneBackupsQuery } from 'data/projects/clone-query'
+import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
+import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
+import { passwordStrength } from 'lib/helpers'
+import { generateStrongPassword } from 'lib/project'
+import {
+ Button,
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogSection,
+ DialogTitle,
+ Form_Shadcn_,
+ FormControl_Shadcn_,
+ FormField_Shadcn_,
+ Input,
+ Input_Shadcn_,
+} from 'ui'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+import { AdditionalMonthlySpend } from './AdditionalMonthlySpend'
+
+interface CreateNewProjectDialogProps {
+ open: boolean
+ selectedBackupId: number | null
+ recoveryTimeTarget: number | null
+ onOpenChange: (value: boolean) => void
+ onCloneSuccess: () => void
+}
+
+export const CreateNewProjectDialog = ({
+ open,
+ selectedBackupId,
+ recoveryTimeTarget,
+ onOpenChange,
+ onCloneSuccess,
+}: CreateNewProjectDialogProps) => {
+ const { project } = useProjectContext()
+ const organization = useSelectedOrganization()
+
+ const [passwordStrengthScore, setPasswordStrengthScore] = useState(0)
+ const [passwordStrengthMessage, setPasswordStrengthMessage] = useState('')
+
+ const FormSchema = z.object({
+ name: z.string().min(1),
+ password: z.string().min(1),
+ })
+
+ const form = useForm>({
+ resolver: zodResolver(FormSchema),
+ defaultValues: {
+ name: '',
+ password: '',
+ },
+ })
+
+ const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug })
+ const isFreePlan = subscription?.plan?.id === 'free'
+
+ const { data: cloneBackups } = useCloneBackupsQuery(
+ { projectRef: project?.ref },
+ { enabled: !isFreePlan }
+ )
+ const hasPITREnabled = cloneBackups?.pitr_enabled
+
+ const { mutate: triggerClone, isLoading: cloneMutationLoading } = useProjectCloneMutation({
+ onError: (error) => {
+ console.error('error', error)
+ toast.error('Failed to restore to new project')
+ },
+ onSuccess: () => {
+ toast.success('Restoration process started')
+ onCloneSuccess()
+ },
+ })
+
+ const delayedCheckPasswordStrength = useRef(
+ debounce((value: string) => checkPasswordStrength(value), 300)
+ ).current
+
+ async function checkPasswordStrength(value: string) {
+ const { message, strength } = await passwordStrength(value)
+ setPasswordStrengthScore(strength)
+ setPasswordStrengthMessage(message)
+ }
+
+ const generatePassword = () => {
+ const password = generateStrongPassword()
+ form.setValue('password', password)
+ delayedCheckPasswordStrength(password)
+ }
+
+ return (
+
+
+
+ Create new project
+
+ This process will create a new project and restore your database to it.
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx
index 8fa10f4f70a..29493fe9edc 100644
--- a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx
+++ b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx
@@ -1,11 +1,9 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { useParams } from 'common'
-import generator from 'generate-password-browser'
import { debounce } from 'lodash'
import { useEffect, useRef, useState } from 'react'
import { toast } from 'sonner'
-import { Button, Input, Modal } from 'ui'
+import { useParams } from 'common'
import {
useIsProjectActive,
useProjectContext,
@@ -17,6 +15,8 @@ import { useDatabasePasswordResetMutation } from 'data/database/database-passwor
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { DEFAULT_MINIMUM_PASSWORD_STRENGTH } from 'lib/constants'
import passwordStrength from 'lib/password-strength'
+import { generateStrongPassword } from 'lib/project'
+import { Button, Input, Modal } from 'ui'
const ResetDbPassword = ({ disabled = false }) => {
const { ref } = useParams()
@@ -80,12 +80,8 @@ const ResetDbPassword = ({ disabled = false }) => {
}
}
- function generateStrongPassword() {
- const password = generator.generate({
- length: 16,
- numbers: true,
- uppercase: true,
- })
+ function generatePassword() {
+ const password = generateStrongPassword()
setPassword(password)
delayedCheckPasswordStrength(password)
}
@@ -148,7 +144,7 @@ const ResetDbPassword = ({ disabled = false }) => {
passwordStrengthScore={passwordStrengthScore}
passwordStrengthMessage={passwordStrengthMessage}
password={password}
- generateStrongPassword={generateStrongPassword}
+ generateStrongPassword={generatePassword}
/>
}
/>
diff --git a/apps/studio/lib/project.tsx b/apps/studio/lib/project.tsx
new file mode 100644
index 00000000000..09f8fba7f30
--- /dev/null
+++ b/apps/studio/lib/project.tsx
@@ -0,0 +1,10 @@
+import generator from 'generate-password-browser'
+
+export const generateStrongPassword = () => {
+ const password = generator.generate({
+ length: 16,
+ numbers: true,
+ uppercase: true,
+ })
+ return password
+}
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 aa0903d6141..f5ecb3174d4 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,5 +1,4 @@
import { useParams } from 'common'
-import generator from 'generate-password-browser'
import { debounce } from 'lodash'
import { useRouter } from 'next/router'
import { ChangeEvent, useRef, useState } from 'react'
@@ -21,6 +20,7 @@ import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { PROVIDERS } from 'lib/constants'
import { getInitialMigrationSQLFromGitHubRepo } from 'lib/integration-utils'
import passwordStrength from 'lib/password-strength'
+import { generateStrongPassword } from 'lib/project'
import { AWS_REGIONS } from 'shared-data'
import { useIntegrationInstallationSnapshot } from 'state/integration-installation'
import type { NextPageWithLayout } from 'types'
@@ -117,13 +117,8 @@ const CreateProject = () => {
setPasswordStrengthMessage(message)
}
- function generateStrongPassword() {
- const password = generator.generate({
- length: 16,
- numbers: true,
- uppercase: true,
- })
-
+ function generatePassword() {
+ const password = generateStrongPassword()
setDbPass(password)
delayedCheckPasswordStrength(password)
}
@@ -245,7 +240,7 @@ const CreateProject = () => {
passwordStrengthScore={passwordStrengthScore}
password={dbPass}
passwordStrengthMessage={passwordStrengthMessage}
- generateStrongPassword={generateStrongPassword}
+ generateStrongPassword={generatePassword}
/>
}
/>
diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx
index 8c2724a60e6..85f50808195 100644
--- a/apps/studio/pages/new/[slug].tsx
+++ b/apps/studio/pages/new/[slug].tsx
@@ -1,6 +1,5 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import generator from 'generate-password-browser'
import { debounce } from 'lodash'
import { ChevronRight, ExternalLink } from 'lucide-react'
import Link from 'next/link'
@@ -27,9 +26,11 @@ import DisabledWarningDueToIncident from 'components/ui/DisabledWarningDueToInci
import Panel from 'components/ui/Panel'
import PartnerManagedResource from 'components/ui/PartnerManagedResource'
import PasswordStrengthBar from 'components/ui/PasswordStrengthBar'
+import { useOverdueInvoicesQuery } from 'data/invoices/invoices-overdue-query'
import { useDefaultRegionQuery } from 'data/misc/get-default-region-query'
import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
+import { instanceSizeSpecs } from 'data/projects/new-project.constants'
import {
DbInstanceSize,
ProjectCreateVariables,
@@ -50,6 +51,7 @@ import {
PROVIDERS,
} from 'lib/constants'
import passwordStrength from 'lib/password-strength'
+import { generateStrongPassword } from 'lib/project'
import type { CloudProvider } from 'shared-data'
import type { NextPageWithLayout } from 'types'
import {
@@ -83,8 +85,6 @@ import { Admonition } from 'ui-patterns/admonition'
import { Input } from 'ui-patterns/DataInputs/Input'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { InfoTooltip } from 'ui-patterns/info-tooltip'
-import { useOverdueInvoicesQuery } from 'data/invoices/invoices-overdue-query'
-import { instanceSizeSpecs } from 'data/projects/new-project.constants'
type DesiredInstanceSize = components['schemas']['DesiredInstanceSize']
@@ -258,15 +258,9 @@ const Wizard: NextPageWithLayout = () => {
// compute credits
10
- // [Joshen] Refactor: DB Password could be a common component
- // used in multiple pages with repeated logic
- function generateStrongPassword() {
- const password = generator.generate({
- length: 16,
- numbers: true,
- uppercase: true,
- })
-
+ // [Refactor] DB Password could be a common component used in multiple pages with repeated logic
+ function generatePassword() {
+ const password = generateStrongPassword()
form.setValue('dbPass', password)
delayedCheckPasswordStrength(password)
}
@@ -759,7 +753,7 @@ const Wizard: NextPageWithLayout = () => {
passwordStrengthScore={form.getValues('dbPassStrength')}
password={field.value}
passwordStrengthMessage={passwordStrengthMessage}
- generateStrongPassword={generateStrongPassword}
+ generateStrongPassword={generatePassword}
/>
}
>
diff --git a/apps/studio/pages/project/[ref]/database/backups/restore-to-new-project.tsx b/apps/studio/pages/project/[ref]/database/backups/restore-to-new-project.tsx
index 632ac45122a..5c9d1c349c0 100644
--- a/apps/studio/pages/project/[ref]/database/backups/restore-to-new-project.tsx
+++ b/apps/studio/pages/project/[ref]/database/backups/restore-to-new-project.tsx
@@ -1,59 +1,34 @@
-import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import BackupsEmpty from 'components/interfaces/Database/Backups/BackupsEmpty'
+import { Loader2 } from 'lucide-react'
+import Link from 'next/link'
+import { useState } from 'react'
+import BackupsEmpty from 'components/interfaces/Database/Backups/BackupsEmpty'
import DatabaseBackupsNav from 'components/interfaces/Database/Backups/DatabaseBackupsNav'
+import { PITRForm } from 'components/interfaces/Database/Backups/PITR/pitr-form'
+import { BackupsList } from 'components/interfaces/Database/Backups/RestoreToNewProject/BackupsList'
+import { ConfirmRestoreDialog } from 'components/interfaces/Database/Backups/RestoreToNewProject/ConfirmRestoreDialog'
+import { CreateNewProjectDialog } from 'components/interfaces/Database/Backups/RestoreToNewProject/CreateNewProjectDialog'
+import { Markdown } from 'components/interfaces/Markdown'
import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import AlertError from 'components/ui/AlertError'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import NoPermission from 'components/ui/NoPermission'
-import Panel from 'components/ui/Panel'
-import PasswordStrengthBar from 'components/ui/PasswordStrengthBar'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import UpgradeToPro from 'components/ui/UpgradeToPro'
-import { useProjectCloneMutation } from 'data/projects/clone-mutation'
import { useCloneBackupsQuery } from 'data/projects/clone-query'
import { useCloneStatusQuery } from 'data/projects/clone-status-query'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { PROJECT_STATUS } from 'lib/constants'
-import { getDatabaseMajorVersion, passwordStrength } from 'lib/helpers'
-import Link from 'next/link'
-import { useState, useRef } from 'react'
-import { useForm } from 'react-hook-form'
+import { getDatabaseMajorVersion } from 'lib/helpers'
import type { NextPageWithLayout } from 'types'
-import {
- Badge,
- Button,
- Dialog,
- DialogContent,
- DialogTitle,
- DialogHeader,
- DialogDescription,
- DialogFooter,
- DialogSection,
- Input,
- Form_Shadcn_,
- FormControl_Shadcn_,
- FormField_Shadcn_,
- Input_Shadcn_,
- Alert_Shadcn_,
- AlertTitle_Shadcn_,
- AlertDescription_Shadcn_,
-} from 'ui'
-import { Admonition, TimestampInfo } from 'ui-patterns'
-import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
-import { z } from 'zod'
-import { debounce } from 'lodash'
-import generator from 'generate-password-browser'
-import { toast } from 'sonner'
-import { Loader2 } from 'lucide-react'
-import { PITRForm } from 'components/interfaces/Database/Backups/PITR/pitr-form'
-import { instanceSizeSpecs } from 'data/projects/new-project.constants'
-import { Markdown } from 'components/interfaces/Markdown'
+import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Button } from 'ui'
+import { Admonition } from 'ui-patterns'
+import { useProjectsQuery } from 'data/projects/projects-query'
const RestoreToNewProjectPage: NextPageWithLayout = () => {
return (
@@ -83,19 +58,20 @@ const RestoreToNewProject = () => {
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug })
const isFreePlan = subscription?.plan?.id === 'free'
+ const [refetchInterval, setRefetchInterval] = useState(false)
+ const [selectedBackupId, setSelectedBackupId] = useState(null)
+ const [showConfirmationDialog, setShowConfirmationDialog] = useState(false)
+ const [showNewProjectDialog, setShowNewProjectDialog] = useState(false)
+ const [recoveryTimeTarget, setRecoveryTimeTarget] = useState(null)
+
+ const { data: projects } = useProjectsQuery()
+
const {
data: cloneBackups,
error,
isLoading: cloneBackupsLoading,
isError,
- } = useCloneBackupsQuery(
- {
- projectRef: project?.ref,
- },
- {
- enabled: !isFreePlan,
- }
- )
+ } = useCloneBackupsQuery({ projectRef: project?.ref }, { enabled: !isFreePlan })
const plan = subscription?.plan?.id
const isActiveHealthy = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY
@@ -116,107 +92,26 @@ const RestoreToNewProject = () => {
data: cloneStatus,
refetch: refetchCloneStatus,
isLoading: cloneStatusLoading,
- } = useCloneStatusQuery({
- projectRef: project?.ref,
- })
+ } = useCloneStatusQuery(
+ {
+ projectRef: project?.ref,
+ },
+ {
+ refetchInterval,
+ refetchOnWindowFocus: false,
+ onSuccess: (data) => {
+ const hasTransientState = data.clones.some((c) => c.status === 'IN_PROGRESS')
+ if (!hasTransientState) setRefetchInterval(false)
+ },
+ }
+ )
const lastClone = cloneStatus?.clones?.[cloneStatus?.clones.length - 1]
- const [selectedBackupId, setSelectedBackupId] = useState(null)
- const [showConfirmationDialog, setShowConfirmationDialog] = useState(false)
- const [showNewProjectDialog, setShowNewProjectDialog] = useState(false)
- const [recoveryTimeTarget, setRecoveryTimeTarget] = useState(null)
const IS_CLONED_PROJECT = (cloneStatus?.cloned_from?.source_project as any)?.ref ? true : false
const isLoading = !isPermissionsLoaded || cloneBackupsLoading || cloneStatusLoading
-
- /**
- * New project will have the same compute size and disk size as the original project
- */
- function getAdditionalMonthlySpend() {
- const currentProjectComputeSize = project?.infra_compute_size
- if (!currentProjectComputeSize) {
- return null
- }
-
- if (currentProjectComputeSize === 'nano') {
- return null
- }
-
- const additionalMonthlySpend = instanceSizeSpecs[currentProjectComputeSize]
-
- return additionalMonthlySpend
- }
-
- const { mutate: triggerClone, isLoading: cloneMutationLoading } = useProjectCloneMutation({
- onError: (error) => {
- console.error('error', error)
- toast.error('Failed to restore to new project')
- },
- onSuccess: () => {
- toast.success('Restoration process started')
- refetchCloneStatus()
- setShowNewProjectDialog(false)
- },
- })
-
- const FormSchema = z.object({
- name: z.string().min(1),
- password: z.string().min(1),
- })
-
- const form = useForm>({
- resolver: zodResolver(FormSchema),
- defaultValues: {
- name: '',
- password: '',
- },
- })
-
- const [passwordStrengthScore, setPasswordStrengthScore] = useState(0)
- const [passwordStrengthMessage, setPasswordStrengthMessage] = useState('')
-
- const delayedCheckPasswordStrength = useRef(
- debounce((value: string) => checkPasswordStrength(value), 300)
- ).current
-
- async function checkPasswordStrength(value: string) {
- const { message, strength } = await passwordStrength(value)
- setPasswordStrengthScore(strength)
- setPasswordStrengthMessage(message)
- }
-
- function generateStrongPassword() {
- const password = generator.generate({
- length: 16,
- numbers: true,
- uppercase: true,
- })
-
- form.setValue('password', password)
- delayedCheckPasswordStrength(password)
- }
-
- function AdditionalMonthlySpend() {
- const additionalMonthlySpend = getAdditionalMonthlySpend()
- if (!additionalMonthlySpend) {
- return null
- }
-
- return (
-
-
- The new project will have the same compute size and disk size as this project. You will be
- able to update the compute size and disk size after the new project is created in{' '}
-
- Project Settings > Compute and Disk
-
-
-
-
Additional Monthly Compute + Disk Cost
-
${additionalMonthlySpend.priceMonthly}
-
-
- )
- }
+ const clonedProject = projects?.find(
+ (p) => p.ref === cloneStatus?.clones?.[0]?.target_project.ref
+ )
if (isLoading) {
return
@@ -237,6 +132,7 @@ const RestoreToNewProject = () => {
title="Restore to new project is not available for this database version"
>
{
)
}
if (IS_CLONED_PROJECT) {
return (
-
+
+
+
+ Go to original project
+
+
)
}
@@ -309,10 +211,16 @@ const RestoreToNewProject = () => {
if (lastClone?.status === 'FAILED') {
return (
-
+
+
+
+ Contact support
+
+
)
}
@@ -321,10 +229,12 @@ const RestoreToNewProject = () => {
return (
+
+ Go to new project
+
)
}
@@ -335,11 +245,13 @@ const RestoreToNewProject = () => {
Restoration in progress
- The new project is being created.
-
-
- Go to new project
-
+
+ The new project{!!clonedProject ? ` ${clonedProject.name}` : ''} is currently being
+ created
+
+
+ Go to new project
+
)
@@ -354,7 +266,7 @@ const RestoreToNewProject = () => {
)
}
@@ -365,169 +277,25 @@ const RestoreToNewProject = () => {
return (
<>
-
-
-
- Confirm restore to a new project
-
- This process will create a new project and restore your database to it.
-
-
-
-
-
- Project organization will stay the same: {organization?.name}
-
-
- Project region will stay the same: {project?.region || ''}
-
-
- A project can only be restored to another project once.
-
- This is a temporary limitation. Please contact us if you need to restore a project
- to multiple other projects.
-
-
-
-
- What will be transferred?
-
- Database schema (tables, views, procedures)
- All data and indexes
- Database roles, permissions and users
-
-
-
- What needs manual reconfiguration?
-
- Storage objects & settings
- Edge Functions
- Auth settings & API keys
- Realtime settings
- Database extensions and settings
- Read replicas
-
-
-
-
-
- setShowConfirmationDialog(false)}>
- Cancel
-
- {
- setShowConfirmationDialog(false)
- setShowNewProjectDialog(true)
- }}
- >
- Continue
-
-
-
-
-
-
-
- Create new project
-
- This process will create a new project and restore your database to it.
-
-
-
-
-
-
-
+ {
+ setShowConfirmationDialog(false)
+ setShowNewProjectDialog(true)
+ }}
+ />
+ {
+ refetchCloneStatus()
+ setRefetchInterval(5000)
+ setShowNewProjectDialog(false)
+ }}
+ />
{hasPITREnabled ? (
<>
{
/>
>
) : (
- <>
-
- {cloneBackups?.backups.length === 0 ? (
- <>
-
- >
- ) : (
-
- {/*
{JSON.stringify({ cloneStatus }, null, 2)} */}
- {cloneBackups?.backups.map((backup) => {
- if (!backup.isPhysicalBackup) return null
- return (
-
-
-
-
-
{JSON.stringify(backup.status).replaceAll('"', '')}
- {(backup.status as any) === 'COMPLETED' && (
-
{
- setSelectedBackupId(backup.id)
- setShowConfirmationDialog(true)
- }}
- >
- Restore
-
- )}
-
- )
- })}
-
- )}
-
- >
+ {
+ setSelectedBackupId(id)
+ setShowConfirmationDialog(true)
+ }}
+ />
)}
>
)
diff --git a/apps/studio/pages/vercel/setupProject.tsx b/apps/studio/pages/vercel/setupProject.tsx
index a9d24ba6070..34e1bab9de7 100644
--- a/apps/studio/pages/vercel/setupProject.tsx
+++ b/apps/studio/pages/vercel/setupProject.tsx
@@ -1,11 +1,9 @@
-import generator from 'generate-password-browser'
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 'sonner'
-import { Button, Input, Listbox } from 'ui'
import VercelIntegrationLayout from 'components/layouts/VercelIntegrationLayout'
import {
@@ -22,9 +20,11 @@ import {
PROVIDERS,
} from 'lib/constants'
import passwordStrength from 'lib/password-strength'
+import { generateStrongPassword } from 'lib/project'
import { VERCEL_INTEGRATION_CONFIGS } from 'lib/vercelConfigs'
import { AWS_REGIONS } from 'shared-data'
import type { Dictionary } from 'types'
+import { Button, Input, Listbox } from 'ui'
interface ISetupProjectStore {
token: string
@@ -211,13 +211,8 @@ const CreateProject = observer(() => {
setPasswordStrengthMessage(message)
}
- function generateStrongPassword() {
- const password = generator.generate({
- length: 16,
- numbers: true,
- uppercase: true,
- })
-
+ function generatePassword() {
+ const password = generateStrongPassword()
setDbPass(password)
delayedCheckPasswordStrength(password)
}
@@ -271,7 +266,7 @@ const CreateProject = observer(() => {
passwordStrengthScore={passwordStrengthScore}
password={dbPass}
passwordStrengthMessage={passwordStrengthMessage}
- generateStrongPassword={generateStrongPassword}
+ generateStrongPassword={generatePassword}
/>
}
/>