CreateNewProjectDialog.tsx199 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import { useState } from 'react'
3import { useForm } from 'react-hook-form'
4import { toast } from 'sonner'
5import {
6 Button,
7 Dialog,
8 DialogContent,
9 DialogDescription,
10 DialogFooter,
11 DialogHeader,
12 DialogSection,
13 DialogTitle,
14 Form,
15 FormControl,
16 FormField,
17 Input,
18} from 'ui'
19import { Input as PasswordInput } from 'ui-patterns/DataInputs/Input'
20import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
21import { z } from 'zod'
22
23import { AdditionalMonthlySpend } from './AdditionalMonthlySpend'
24import { NewProjectPrice } from './RestoreToNewProject.utils'
25import { PasswordStrengthBar } from '@/components/ui/PasswordStrengthBar'
26import { useProjectCloneMutation } from '@/data/projects/clone-mutation'
27import { useCloneBackupsQuery } from '@/data/projects/clone-query'
28import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
29import { passwordStrength, PasswordStrengthScore } from '@/lib/password-strength'
30import { generateStrongPassword } from '@/lib/project'
31
32interface CreateNewProjectDialogProps {
33 open: boolean
34 selectedBackupId: number | null
35 recoveryTimeTarget: number | null
36 onOpenChange: (value: boolean) => void
37 onCloneSuccess: () => void
38 additionalMonthlySpend: NewProjectPrice
39 hasAccess?: boolean
40}
41
42export const CreateNewProjectDialog = ({
43 open,
44 selectedBackupId,
45 recoveryTimeTarget,
46 onOpenChange,
47 onCloneSuccess,
48 additionalMonthlySpend,
49 hasAccess,
50}: CreateNewProjectDialogProps) => {
51 const { data: project } = useSelectedProjectQuery()
52 const [passwordStrengthScore, setPasswordStrengthScore] = useState(0)
53 const [passwordStrengthMessage, setPasswordStrengthMessage] = useState('')
54
55 const FormSchema = z.object({
56 name: z.string().min(1),
57 password: z.string().min(1),
58 })
59
60 const form = useForm<z.infer<typeof FormSchema>>({
61 resolver: zodResolver(FormSchema as any),
62 defaultValues: {
63 name: '',
64 password: '',
65 },
66 })
67
68 const { data: cloneBackups } = useCloneBackupsQuery(
69 { projectRef: project?.ref },
70 { enabled: hasAccess }
71 )
72 const hasPITREnabled = cloneBackups?.pitr_enabled
73
74 const { mutate: triggerClone, isPending: cloneMutationLoading } = useProjectCloneMutation({
75 onError: (error) => {
76 toast.error(`Failed to restore to new project: ${error.message}`)
77 },
78 onSuccess: () => {
79 toast.success('Restoration process started')
80 onCloneSuccess()
81 },
82 })
83
84 async function checkPasswordStrength(value: string) {
85 const { message, strength } = await passwordStrength(value)
86 setPasswordStrengthScore(strength)
87 setPasswordStrengthMessage(message)
88 }
89
90 const generatePassword = () => {
91 const password = generateStrongPassword()
92 form.setValue('password', password)
93 checkPasswordStrength(password)
94 }
95
96 return (
97 <Dialog open={open} onOpenChange={onOpenChange}>
98 <DialogContent>
99 <DialogHeader className="border-b">
100 <DialogTitle>Create new project</DialogTitle>
101 <DialogDescription>
102 This process will create a new project and restore your database to it.
103 </DialogDescription>
104 </DialogHeader>
105 <Form {...form}>
106 <form
107 id={'create-new-project-form'}
108 onSubmit={form.handleSubmit((data) => {
109 if (!project?.ref) {
110 toast.error('Project ref is required')
111 return
112 }
113
114 if (hasPITREnabled && recoveryTimeTarget) {
115 triggerClone({
116 projectRef: project?.ref,
117 newProjectName: data.name,
118 newDbPass: data.password,
119 recoveryTimeTarget: recoveryTimeTarget,
120 cloneBackupId: undefined,
121 })
122 } else if (selectedBackupId) {
123 triggerClone({
124 projectRef: project?.ref,
125 cloneBackupId: selectedBackupId,
126 newProjectName: data.name,
127 newDbPass: data.password,
128 recoveryTimeTarget: undefined,
129 })
130 } else {
131 toast.error('No backup or point in time selected')
132 return
133 }
134 })}
135 >
136 <DialogSection className="pb-6 space-y-4 text-sm">
137 <FormField
138 control={form.control}
139 name="name"
140 render={({ field }) => (
141 <FormItemLayout label="New Project Name">
142 <FormControl>
143 <Input placeholder="Enter a name" type="text" {...field} />
144 </FormControl>
145 </FormItemLayout>
146 )}
147 />
148 <FormField
149 control={form.control}
150 name="password"
151 render={({ field }) => (
152 <FormItemLayout
153 label="Database password"
154 description={
155 <PasswordStrengthBar
156 passwordStrengthScore={passwordStrengthScore as PasswordStrengthScore}
157 password={field.value}
158 passwordStrengthMessage={passwordStrengthMessage}
159 generateStrongPassword={generatePassword}
160 />
161 }
162 >
163 <FormControl>
164 <PasswordInput
165 id="db-password"
166 type="password"
167 placeholder="Type in a strong password"
168 value={field.value}
169 copy={field.value?.length > 0}
170 reveal
171 onChange={(e) => {
172 const value = e.target.value
173 field.onChange(value)
174 if (value == '') {
175 setPasswordStrengthScore(-1)
176 setPasswordStrengthMessage('')
177 } else checkPasswordStrength(value)
178 }}
179 />
180 </FormControl>
181 </FormItemLayout>
182 )}
183 />
184 </DialogSection>
185 <AdditionalMonthlySpend additionalMonthlySpend={additionalMonthlySpend} />
186 <DialogFooter>
187 <Button htmlType="reset" type="outline" onClick={() => onOpenChange(false)}>
188 Cancel
189 </Button>
190 <Button htmlType="submit" loading={cloneMutationLoading}>
191 Restore to new project
192 </Button>
193 </DialogFooter>
194 </form>
195 </Form>
196 </DialogContent>
197 </Dialog>
198 )
199}