CreateNewProjectDialog.tsx 6.7 KB

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