mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 01:15:03 +03:00
Refactor restore to new project by breaking into smaller components (#30604)
* Refactor restore to new project by breaking into smaller components * Shift generateStrongPassword into a utils * Update each notice component in restore to new project to be more consistent * Add long polling in restore to new project * Call refetch
This commit is contained in:
1 parent
8c72360707
commit
ecf001ab90
11 files changed
+525
-404
No files matched your search
@@ -32,7 +32,7 @@ function DatabaseBackupsNav({ active }: Props) {
|
||||
label: (
|
||||
<div className="flex items-center gap-1">
|
||||
Restore to new project{' '}
|
||||
<Badge size="small" className="!text-[10px] px-1 py-0">
|
||||
<Badge size="small" className="!text-[10px] px-1.5 py-0">
|
||||
New
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
+45
@@ -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 (
|
||||
<div className="text-sm text-foreground-lighter border-t p-5">
|
||||
<p>
|
||||
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{' '}
|
||||
<span className="font-mono text-xs tracking-tighter text-foreground-light">
|
||||
Project Settings > Compute and Disk
|
||||
</span>
|
||||
</p>
|
||||
<div className="flex justify-between text-foreground mt-2">
|
||||
<p>Additional Monthly Compute + Disk Cost</p>
|
||||
<p className="font-mono text-right text-brand">${additionalMonthlySpend.priceMonthly}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+59
@@ -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 (
|
||||
<Panel>
|
||||
{cloneBackups?.backups.length === 0 ? (
|
||||
<>
|
||||
<BackupsEmpty />
|
||||
</>
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
{/* <pre>{JSON.stringify({ cloneStatus }, null, 2)}</pre> */}
|
||||
{cloneBackups?.backups.map((backup) => {
|
||||
if (!backup.isPhysicalBackup) return null
|
||||
return (
|
||||
<div className="flex p-4 gap-4" key={backup.id}>
|
||||
<div>
|
||||
<TimestampInfo value={backup.inserted_at} />
|
||||
</div>
|
||||
<Badge>{JSON.stringify(backup.status).replaceAll('"', '')}</Badge>
|
||||
{(backup.status as any) === 'COMPLETED' && (
|
||||
<Button
|
||||
className="ml-auto"
|
||||
type="outline"
|
||||
onClick={() => onSelectRestore(backup.id)}
|
||||
>
|
||||
Restore
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
+84
@@ -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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader className="border-b">
|
||||
<DialogTitle>Confirm restore to a new project</DialogTitle>
|
||||
<DialogDescription>
|
||||
This process will create a new project and restore your database to it.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogSection className="prose pb-6 space-y-4 text-sm">
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
Project organization will stay the same: <code>{organization?.name}</code>
|
||||
</li>
|
||||
<li>
|
||||
Project region will stay the same: <code>{project?.region || ''}</code>
|
||||
</li>
|
||||
<li>
|
||||
A project can only be restored to another project once. <br />
|
||||
<span className="text-foreground-lighter text-xs">
|
||||
This is a temporary limitation. Please contact us if you need to restore a project
|
||||
to multiple other projects.
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>What will be transferred?</li>
|
||||
<ul className="ml-4">
|
||||
<li>Database schema (tables, views, procedures)</li>
|
||||
<li>All data and indexes</li>
|
||||
<li>Database roles, permissions and users</li>
|
||||
</ul>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>What needs manual reconfiguration?</li>
|
||||
<ul className="ml-4">
|
||||
<li>Storage objects & settings</li>
|
||||
<li>Edge Functions</li>
|
||||
<li>Auth settings & API keys</li>
|
||||
<li>Realtime settings</li>
|
||||
<li>Database extensions and settings</li>
|
||||
<li>Read replicas</li>
|
||||
</ul>
|
||||
</ul>
|
||||
</DialogSection>
|
||||
<AdditionalMonthlySpend />
|
||||
<DialogFooter>
|
||||
<Button type="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => onSelectContinue()}>Continue</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
+205
@@ -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<z.infer<typeof FormSchema>>({
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader className="border-b">
|
||||
<DialogTitle>Create new project</DialogTitle>
|
||||
<DialogDescription>
|
||||
This process will create a new project and restore your database to it.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form
|
||||
id={'create-new-project-form'}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
if (!project?.ref) {
|
||||
toast.error('Project ref is required')
|
||||
return
|
||||
}
|
||||
|
||||
if (hasPITREnabled && recoveryTimeTarget) {
|
||||
triggerClone({
|
||||
projectRef: project?.ref,
|
||||
newProjectName: data.name,
|
||||
newDbPass: data.password,
|
||||
recoveryTimeTarget: recoveryTimeTarget,
|
||||
cloneBackupId: undefined,
|
||||
})
|
||||
} else if (selectedBackupId) {
|
||||
triggerClone({
|
||||
projectRef: project?.ref,
|
||||
cloneBackupId: selectedBackupId,
|
||||
newProjectName: data.name,
|
||||
newDbPass: data.password,
|
||||
recoveryTimeTarget: undefined,
|
||||
})
|
||||
} else {
|
||||
toast.error('No backup or point in time selected')
|
||||
return
|
||||
}
|
||||
})}
|
||||
>
|
||||
<DialogSection className="pb-6 space-y-4 text-sm">
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout label="New Project Name">
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_ placeholder="Enter a name" type="text" {...field} />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout>
|
||||
<FormControl_Shadcn_>
|
||||
<Input
|
||||
id="db-password"
|
||||
label="Database Password"
|
||||
type="password"
|
||||
placeholder="Type in a strong password"
|
||||
value={field.value}
|
||||
copy={field.value?.length > 0}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
field.onChange(value)
|
||||
if (value == '') {
|
||||
setPasswordStrengthScore(-1)
|
||||
setPasswordStrengthMessage('')
|
||||
} else delayedCheckPasswordStrength(value)
|
||||
}}
|
||||
descriptionText={
|
||||
<PasswordStrengthBar
|
||||
passwordStrengthScore={passwordStrengthScore}
|
||||
password={field.value}
|
||||
passwordStrengthMessage={passwordStrengthMessage}
|
||||
generateStrongPassword={generatePassword}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</DialogSection>
|
||||
<AdditionalMonthlySpend />
|
||||
<DialogFooter>
|
||||
<Button htmlType="reset" type="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button htmlType="submit" loading={cloneMutationLoading}>
|
||||
Restore to new project
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
+6
-10
@@ -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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -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<number | false>(false)
|
||||
const [selectedBackupId, setSelectedBackupId] = useState<number | null>(null)
|
||||
const [showConfirmationDialog, setShowConfirmationDialog] = useState(false)
|
||||
const [showNewProjectDialog, setShowNewProjectDialog] = useState(false)
|
||||
const [recoveryTimeTarget, setRecoveryTimeTarget] = useState<number | null>(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<number | null>(null)
|
||||
const [showConfirmationDialog, setShowConfirmationDialog] = useState(false)
|
||||
const [showNewProjectDialog, setShowNewProjectDialog] = useState(false)
|
||||
const [recoveryTimeTarget, setRecoveryTimeTarget] = useState<number | null>(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<z.infer<typeof FormSchema>>({
|
||||
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 (
|
||||
<div className="text-sm text-foreground-lighter border-t p-5">
|
||||
<p>
|
||||
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{' '}
|
||||
<span className="font-mono text-xs tracking-tighter text-foreground-light">
|
||||
Project Settings > Compute and Disk
|
||||
</span>
|
||||
</p>
|
||||
<div className="flex justify-between text-foreground mt-2">
|
||||
<p>Additional Monthly Compute + Disk Cost</p>
|
||||
<p className="font-mono text-right text-brand">${additionalMonthlySpend.priceMonthly}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const clonedProject = projects?.find(
|
||||
(p) => p.ref === cloneStatus?.clones?.[0]?.target_project.ref
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
return <GenericSkeletonLoader />
|
||||
@@ -237,6 +132,7 @@ const RestoreToNewProject = () => {
|
||||
title="Restore to new project is not available for this database version"
|
||||
>
|
||||
<Markdown
|
||||
className="max-w-full"
|
||||
content={`Restore to new project is only available for Postgres 15 and above.
|
||||
Go to [infrastructure settings](/project/${project?.ref}/settings/infrastructure)
|
||||
to upgrade your database version.
|
||||
@@ -274,20 +170,26 @@ const RestoreToNewProject = () => {
|
||||
<UpgradeToPro
|
||||
buttonText="Upgrade"
|
||||
primaryText="Restore to a new project requires a pro plan or above."
|
||||
secondaryText={
|
||||
'To restore to a new project, you need to upgrade to a Pro plan and have physical backups enabled.'
|
||||
}
|
||||
secondaryText="To restore to a new project, you need to upgrade to a Pro plan and have physical backups enabled."
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (IS_CLONED_PROJECT) {
|
||||
return (
|
||||
<Admonition type="default" title={`This project cannot be restored to a new project`}>
|
||||
<Admonition type="default" title="This project cannot be restored to a new project">
|
||||
<Markdown
|
||||
content={`This project was originally restored from another project. This is a temporary limitation. Please [contact us](/support/new?ref=${project?.ref}) if you need to restore a project to multiple other projects.
|
||||
[Go to original project](/dashboard/project/${(cloneStatus?.cloned_from?.source_project as any)?.ref || ''})`}
|
||||
className="max-w-full [&>p]:!leading-normal"
|
||||
content={`This is a temporary limitation whereby projects that were originally restored from another project cannot be restored to yet another project.
|
||||
If you need to restore a project to multiple other projects, please reach out via [support](/support/new?ref=${project?.ref}).`}
|
||||
/>
|
||||
<Button asChild type="default">
|
||||
<Link
|
||||
href={`/dashboard/project/${(cloneStatus?.cloned_from?.source_project as any)?.ref || ''}`}
|
||||
>
|
||||
Go to original project
|
||||
</Link>
|
||||
</Button>
|
||||
</Admonition>
|
||||
)
|
||||
}
|
||||
@@ -309,10 +211,16 @@ const RestoreToNewProject = () => {
|
||||
if (lastClone?.status === 'FAILED') {
|
||||
return (
|
||||
<Admonition type="destructive" title="Failed to restore to new project">
|
||||
<Markdown
|
||||
content="The new project failed to be created.
|
||||
[Contact support](/support/new?category=dashboard_bug)"
|
||||
/>
|
||||
<Markdown content="Sorry! The new project failed to be created, please reach out to support for assistance." />
|
||||
<Button asChild type="default">
|
||||
<Link
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href={`/support/new?category=dashboard_bug&subject=Failed%20to%20restore%20to%20new%20project&message=Target%20project%20reference:%20${clonedProject?.ref ?? 'unknown'}`}
|
||||
>
|
||||
Contact support
|
||||
</Link>
|
||||
</Button>
|
||||
</Admonition>
|
||||
)
|
||||
}
|
||||
@@ -321,10 +229,12 @@ const RestoreToNewProject = () => {
|
||||
return (
|
||||
<Admonition type="default" title="Restoration completed">
|
||||
<Markdown
|
||||
content={`The new project has been created. A project can only be restored to another project once.
|
||||
[Go to new project](/project/${lastClone?.target_project.ref})
|
||||
`}
|
||||
className="max-w-full"
|
||||
content={`The new project${!!clonedProject ? ` ${clonedProject.name}` : ''} has been created. A project can only be restored to another project once.`}
|
||||
/>
|
||||
<Button asChild type="default">
|
||||
<Link href={`/project/${lastClone?.target_project.ref}`}>Go to new project</Link>
|
||||
</Button>
|
||||
</Admonition>
|
||||
)
|
||||
}
|
||||
@@ -335,11 +245,13 @@ const RestoreToNewProject = () => {
|
||||
<Loader2 className="animate-spin" />
|
||||
<AlertTitle_Shadcn_>Restoration in progress</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
The new project is being created.
|
||||
<br />
|
||||
<Link className="underline" href={`/project/${lastClone?.target_project.ref}`}>
|
||||
Go to new project
|
||||
</Link>
|
||||
<p>
|
||||
The new project{!!clonedProject ? ` ${clonedProject.name}` : ''} is currently being
|
||||
created
|
||||
</p>
|
||||
<Button asChild type="default" className="mt-2">
|
||||
<Link href={`/project/${lastClone?.target_project.ref}`}>Go to new project</Link>
|
||||
</Button>
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
)
|
||||
@@ -354,7 +266,7 @@ const RestoreToNewProject = () => {
|
||||
<Admonition
|
||||
type="default"
|
||||
title="No backups found"
|
||||
description={'PITR is enabled, but no backups were found. Check again in a few minutes.'}
|
||||
description="PITR is enabled, but no backups were found. Check again in a few minutes."
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -365,169 +277,25 @@ const RestoreToNewProject = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={showConfirmationDialog} onOpenChange={setShowConfirmationDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader className="border-b">
|
||||
<DialogTitle>Confirm restore to a new project</DialogTitle>
|
||||
<DialogDescription>
|
||||
This process will create a new project and restore your database to it.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogSection className="prose pb-6 space-y-4 text-sm">
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
Project organization will stay the same: <code>{organization?.name}</code>
|
||||
</li>
|
||||
<li>
|
||||
Project region will stay the same: <code>{project?.region || ''}</code>
|
||||
</li>
|
||||
<li>
|
||||
A project can only be restored to another project once. <br />
|
||||
<span className="text-foreground-lighter text-xs">
|
||||
This is a temporary limitation. Please contact us if you need to restore a project
|
||||
to multiple other projects.
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>What will be transferred?</li>
|
||||
<ul className="ml-4">
|
||||
<li>Database schema (tables, views, procedures)</li>
|
||||
<li>All data and indexes</li>
|
||||
<li>Database roles, permissions and users</li>
|
||||
</ul>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>What needs manual reconfiguration?</li>
|
||||
<ul className="ml-4">
|
||||
<li>Storage objects & settings</li>
|
||||
<li>Edge Functions</li>
|
||||
<li>Auth settings & API keys</li>
|
||||
<li>Realtime settings</li>
|
||||
<li>Database extensions and settings</li>
|
||||
<li>Read replicas</li>
|
||||
</ul>
|
||||
</ul>
|
||||
</DialogSection>
|
||||
<AdditionalMonthlySpend />
|
||||
<DialogFooter>
|
||||
<Button type="outline" onClick={() => setShowConfirmationDialog(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowConfirmationDialog(false)
|
||||
setShowNewProjectDialog(true)
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Dialog open={showNewProjectDialog} onOpenChange={setShowNewProjectDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader className="border-b">
|
||||
<DialogTitle>Create new project</DialogTitle>
|
||||
<DialogDescription>
|
||||
This process will create a new project and restore your database to it.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form
|
||||
id={'create-new-project-form'}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
if (!project?.ref) {
|
||||
toast.error('Project ref is required')
|
||||
return
|
||||
}
|
||||
|
||||
if (hasPITREnabled && recoveryTimeTarget) {
|
||||
triggerClone({
|
||||
projectRef: project?.ref,
|
||||
newProjectName: data.name,
|
||||
newDbPass: data.password,
|
||||
recoveryTimeTarget: recoveryTimeTarget,
|
||||
cloneBackupId: undefined,
|
||||
})
|
||||
} else if (selectedBackupId) {
|
||||
triggerClone({
|
||||
projectRef: project?.ref,
|
||||
cloneBackupId: selectedBackupId,
|
||||
newProjectName: data.name,
|
||||
newDbPass: data.password,
|
||||
recoveryTimeTarget: undefined,
|
||||
})
|
||||
} else {
|
||||
toast.error('No backup or point in time selected')
|
||||
return
|
||||
}
|
||||
})}
|
||||
>
|
||||
<DialogSection className="pb-6 space-y-4 text-sm">
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout label="New Project Name">
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_ placeholder="Enter a name" type="text" {...field} />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout>
|
||||
<FormControl_Shadcn_>
|
||||
<Input
|
||||
id="db-password"
|
||||
label="Database Password"
|
||||
type="password"
|
||||
placeholder="Type in a strong password"
|
||||
value={field.value}
|
||||
copy={field.value?.length > 0}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
field.onChange(value)
|
||||
if (value == '') {
|
||||
setPasswordStrengthScore(-1)
|
||||
setPasswordStrengthMessage('')
|
||||
} else delayedCheckPasswordStrength(value)
|
||||
}}
|
||||
descriptionText={
|
||||
<PasswordStrengthBar
|
||||
passwordStrengthScore={passwordStrengthScore}
|
||||
password={field.value}
|
||||
passwordStrengthMessage={passwordStrengthMessage}
|
||||
generateStrongPassword={generateStrongPassword}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</DialogSection>
|
||||
<AdditionalMonthlySpend />
|
||||
<DialogFooter>
|
||||
<Button
|
||||
htmlType="reset"
|
||||
type="outline"
|
||||
onClick={() => setShowNewProjectDialog(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button htmlType="submit" loading={cloneMutationLoading}>
|
||||
Restore to new project
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<ConfirmRestoreDialog
|
||||
open={showConfirmationDialog}
|
||||
onOpenChange={setShowConfirmationDialog}
|
||||
onSelectContinue={() => {
|
||||
setShowConfirmationDialog(false)
|
||||
setShowNewProjectDialog(true)
|
||||
}}
|
||||
/>
|
||||
<CreateNewProjectDialog
|
||||
open={showNewProjectDialog}
|
||||
selectedBackupId={selectedBackupId}
|
||||
recoveryTimeTarget={recoveryTimeTarget}
|
||||
onOpenChange={setShowNewProjectDialog}
|
||||
onCloneSuccess={() => {
|
||||
refetchCloneStatus()
|
||||
setRefetchInterval(5000)
|
||||
setShowNewProjectDialog(false)
|
||||
}}
|
||||
/>
|
||||
{hasPITREnabled ? (
|
||||
<>
|
||||
<PITRForm
|
||||
@@ -544,42 +312,12 @@ const RestoreToNewProject = () => {
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Panel>
|
||||
{cloneBackups?.backups.length === 0 ? (
|
||||
<>
|
||||
<BackupsEmpty />
|
||||
</>
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
{/* <pre>{JSON.stringify({ cloneStatus }, null, 2)}</pre> */}
|
||||
{cloneBackups?.backups.map((backup) => {
|
||||
if (!backup.isPhysicalBackup) return null
|
||||
return (
|
||||
<div className="flex p-4 gap-4" key={backup.id}>
|
||||
<div>
|
||||
<TimestampInfo value={backup.inserted_at} />
|
||||
</div>
|
||||
<Badge>{JSON.stringify(backup.status).replaceAll('"', '')}</Badge>
|
||||
{(backup.status as any) === 'COMPLETED' && (
|
||||
<Button
|
||||
className="ml-auto"
|
||||
type="outline"
|
||||
onClick={() => {
|
||||
setSelectedBackupId(backup.id)
|
||||
setShowConfirmationDialog(true)
|
||||
}}
|
||||
>
|
||||
Restore
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
</>
|
||||
<BackupsList
|
||||
onSelectRestore={(id) => {
|
||||
setSelectedBackupId(id)
|
||||
setShowConfirmationDialog(true)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user