| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199 |
- import { zodResolver } from '@hookform/resolvers/zod'
- import { useState } from 'react'
- import { useForm } from 'react-hook-form'
- import { toast } from 'sonner'
- import {
- Button,
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogSection,
- DialogTitle,
- Form,
- FormControl,
- FormField,
- Input,
- } from 'ui'
- import { Input as PasswordInput } from 'ui-patterns/DataInputs/Input'
- import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
- import { z } from 'zod'
- import { AdditionalMonthlySpend } from './AdditionalMonthlySpend'
- import { NewProjectPrice } from './RestoreToNewProject.utils'
- import { PasswordStrengthBar } from '@/components/ui/PasswordStrengthBar'
- import { useProjectCloneMutation } from '@/data/projects/clone-mutation'
- import { useCloneBackupsQuery } from '@/data/projects/clone-query'
- import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
- import { passwordStrength, PasswordStrengthScore } from '@/lib/password-strength'
- import { generateStrongPassword } from '@/lib/project'
- interface CreateNewProjectDialogProps {
- open: boolean
- selectedBackupId: number | null
- recoveryTimeTarget: number | null
- onOpenChange: (value: boolean) => void
- onCloneSuccess: () => void
- additionalMonthlySpend: NewProjectPrice
- hasAccess?: boolean
- }
- export const CreateNewProjectDialog = ({
- open,
- selectedBackupId,
- recoveryTimeTarget,
- onOpenChange,
- onCloneSuccess,
- additionalMonthlySpend,
- hasAccess,
- }: CreateNewProjectDialogProps) => {
- const { data: project } = useSelectedProjectQuery()
- 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 as any),
- defaultValues: {
- name: '',
- password: '',
- },
- })
- const { data: cloneBackups } = useCloneBackupsQuery(
- { projectRef: project?.ref },
- { enabled: hasAccess }
- )
- const hasPITREnabled = cloneBackups?.pitr_enabled
- const { mutate: triggerClone, isPending: cloneMutationLoading } = useProjectCloneMutation({
- onError: (error) => {
- toast.error(`Failed to restore to new project: ${error.message}`)
- },
- onSuccess: () => {
- toast.success('Restoration process started')
- onCloneSuccess()
- },
- })
- async function checkPasswordStrength(value: string) {
- const { message, strength } = await passwordStrength(value)
- setPasswordStrengthScore(strength)
- setPasswordStrengthMessage(message)
- }
- const generatePassword = () => {
- const password = generateStrongPassword()
- form.setValue('password', password)
- checkPasswordStrength(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 {...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
- control={form.control}
- name="name"
- render={({ field }) => (
- <FormItemLayout label="New Project Name">
- <FormControl>
- <Input placeholder="Enter a name" type="text" {...field} />
- </FormControl>
- </FormItemLayout>
- )}
- />
- <FormField
- control={form.control}
- name="password"
- render={({ field }) => (
- <FormItemLayout
- label="Database password"
- description={
- <PasswordStrengthBar
- passwordStrengthScore={passwordStrengthScore as PasswordStrengthScore}
- password={field.value}
- passwordStrengthMessage={passwordStrengthMessage}
- generateStrongPassword={generatePassword}
- />
- }
- >
- <FormControl>
- <PasswordInput
- id="db-password"
- type="password"
- placeholder="Type in a strong password"
- value={field.value}
- copy={field.value?.length > 0}
- reveal
- onChange={(e) => {
- const value = e.target.value
- field.onChange(value)
- if (value == '') {
- setPasswordStrengthScore(-1)
- setPasswordStrengthMessage('')
- } else checkPasswordStrength(value)
- }}
- />
- </FormControl>
- </FormItemLayout>
- )}
- />
- </DialogSection>
- <AdditionalMonthlySpend additionalMonthlySpend={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>
- </DialogContent>
- </Dialog>
- )
- }
|