From 2ddf3c6597d7ed4b16f60070d317090621aa7ebc Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Wed, 10 Dec 2025 21:16:20 +0100 Subject: [PATCH] refactor: split forms (#124) * refactor: split create-volume-form * refactor: split create-repository-form * chore: pr feedbacks --- .../components/create-repository-form.tsx | 793 ------------------ app/client/components/create-volume-form.tsx | 709 ---------------- .../components/create-repository-form.tsx | 275 ++++++ .../azure-repository-form.tsx | 78 ++ .../repository-forms/gcs-repository-form.tsx | 65 ++ .../components/repository-forms/index.ts | 8 + .../local-repository-form.tsx | 103 +++ .../repository-forms/r2-repository-form.tsx | 80 ++ .../rclone-repository-form.tsx | 96 +++ .../repository-forms/rest-repository-form.tsx | 78 ++ .../repository-forms/s3-repository-form.tsx | 78 ++ .../repository-forms/sftp-repository-form.tsx | 101 +++ .../repositories/routes/create-repository.tsx | 5 +- .../volumes/components/create-volume-form.tsx | 218 +++++ .../volume-forms/directory-form.tsx | 51 ++ .../volumes/components/volume-forms/index.ts | 5 + .../components/volume-forms/nfs-form.tsx | 120 +++ .../components/volume-forms/rclone-form.tsx | 127 +++ .../components/volume-forms/smb-form.tsx | 163 ++++ .../components/volume-forms/webdav-form.tsx | 146 ++++ .../modules/volumes/routes/create-volume.tsx | 2 +- app/client/modules/volumes/tabs/info.tsx | 2 +- 22 files changed, 1798 insertions(+), 1505 deletions(-) delete mode 100644 app/client/components/create-repository-form.tsx delete mode 100644 app/client/components/create-volume-form.tsx create mode 100644 app/client/modules/repositories/components/create-repository-form.tsx create mode 100644 app/client/modules/repositories/components/repository-forms/azure-repository-form.tsx create mode 100644 app/client/modules/repositories/components/repository-forms/gcs-repository-form.tsx create mode 100644 app/client/modules/repositories/components/repository-forms/index.ts create mode 100644 app/client/modules/repositories/components/repository-forms/local-repository-form.tsx create mode 100644 app/client/modules/repositories/components/repository-forms/r2-repository-form.tsx create mode 100644 app/client/modules/repositories/components/repository-forms/rclone-repository-form.tsx create mode 100644 app/client/modules/repositories/components/repository-forms/rest-repository-form.tsx create mode 100644 app/client/modules/repositories/components/repository-forms/s3-repository-form.tsx create mode 100644 app/client/modules/repositories/components/repository-forms/sftp-repository-form.tsx create mode 100644 app/client/modules/volumes/components/create-volume-form.tsx create mode 100644 app/client/modules/volumes/components/volume-forms/directory-form.tsx create mode 100644 app/client/modules/volumes/components/volume-forms/index.ts create mode 100644 app/client/modules/volumes/components/volume-forms/nfs-form.tsx create mode 100644 app/client/modules/volumes/components/volume-forms/rclone-form.tsx create mode 100644 app/client/modules/volumes/components/volume-forms/smb-form.tsx create mode 100644 app/client/modules/volumes/components/volume-forms/webdav-form.tsx diff --git a/app/client/components/create-repository-form.tsx b/app/client/components/create-repository-form.tsx deleted file mode 100644 index ad013dbe..00000000 --- a/app/client/components/create-repository-form.tsx +++ /dev/null @@ -1,793 +0,0 @@ -import { arktypeResolver } from "@hookform/resolvers/arktype"; -import { type } from "arktype"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { Check, Pencil, Save, X } from "lucide-react"; -import { cn, slugify } from "~/client/lib/utils"; -import { deepClean } from "~/utils/object"; -import { Button } from "./ui/button"; -import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./ui/form"; -import { Input } from "./ui/input"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; -import { useQuery } from "@tanstack/react-query"; -import { Alert, AlertDescription } from "./ui/alert"; -import { ExternalLink, AlertTriangle } from "lucide-react"; -import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; -import { useSystemInfo } from "~/client/hooks/use-system-info"; -import { COMPRESSION_MODES, repositoryConfigSchema } from "~/schemas/restic"; -import { listRcloneRemotesOptions } from "../api-client/@tanstack/react-query.gen"; -import { Checkbox } from "./ui/checkbox"; -import { DirectoryBrowser } from "./directory-browser"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "./ui/alert-dialog"; -import { Textarea } from "./ui/textarea"; - -export const formSchema = type({ - name: "2<=string<=32", - compressionMode: type.valueOf(COMPRESSION_MODES).optional(), -}).and(repositoryConfigSchema); -const cleanSchema = type.pipe((d) => formSchema(deepClean(d))); - -export type RepositoryFormValues = typeof formSchema.inferIn; - -type Props = { - onSubmit: (values: RepositoryFormValues) => void; - mode?: "create" | "update"; - initialValues?: Partial; - formId?: string; - loading?: boolean; - className?: string; -}; - -const defaultValuesForType = { - local: { backend: "local" as const, compressionMode: "auto" as const }, - s3: { backend: "s3" as const, compressionMode: "auto" as const }, - r2: { backend: "r2" as const, compressionMode: "auto" as const }, - gcs: { backend: "gcs" as const, compressionMode: "auto" as const }, - azure: { backend: "azure" as const, compressionMode: "auto" as const }, - rclone: { backend: "rclone" as const, compressionMode: "auto" as const }, - rest: { backend: "rest" as const, compressionMode: "auto" as const }, - sftp: { backend: "sftp" as const, compressionMode: "auto" as const, port: 22 }, -}; - -export const CreateRepositoryForm = ({ - onSubmit, - mode = "create", - initialValues, - formId, - loading, - className, -}: Props) => { - const form = useForm({ - resolver: arktypeResolver(cleanSchema as unknown as typeof formSchema), - defaultValues: initialValues, - resetOptions: { - keepDefaultValues: true, - keepDirtyValues: false, - }, - }); - - const { watch, setValue } = form; - - const watchedBackend = watch("backend"); - const watchedIsExistingRepository = watch("isExistingRepository"); - - const [passwordMode, setPasswordMode] = useState<"default" | "custom">("default"); - const [showPathBrowser, setShowPathBrowser] = useState(false); - const [showPathWarning, setShowPathWarning] = useState(false); - - const { capabilities } = useSystemInfo(); - - const { data: rcloneRemotes, isLoading: isLoadingRemotes } = useQuery({ - ...listRcloneRemotesOptions(), - enabled: capabilities.rclone, - }); - - useEffect(() => { - form.reset({ - name: form.getValues().name, - isExistingRepository: form.getValues().isExistingRepository, - customPassword: form.getValues().customPassword, - ...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType], - }); - }, [watchedBackend, form]); - - return ( -
- - ( - - Name - - field.onChange(slugify(e.target.value))} - max={32} - min={2} - /> - - Unique identifier for the repository. - - - )} - /> - ( - - Backend - - Choose the storage backend for this repository. - - - )} - /> - - ( - - Compression Mode - - Compression mode for backups stored in this repository. - - - )} - /> - - ( - - - { - field.onChange(checked); - if (!checked) { - setPasswordMode("default"); - setValue("customPassword", undefined); - } - }} - /> - -
- Import existing repository - Check this if the repository already exists at the specified location -
-
- )} - /> - {watchedIsExistingRepository && ( - <> - - Repository Password - - - Choose whether to use Zerobyte's master password or enter a custom password for the existing repository. - - - - {passwordMode === "custom" && ( - ( - - Repository Password - - - - - The password used to encrypt this repository. It will be stored securely. - - - - )} - /> - )} - - )} - - {watchedBackend === "local" && ( - <> - - Repository Directory -
-
- {form.watch("path") || "/var/lib/zerobyte/repositories"} -
- -
- The directory where the repository will be stored. -
- - - - - - - Important: Host mount required - - -

When selecting a custom path, ensure it is mounted from the host machine into the container.

-

- If the path is not a host mount, you will lose your repository data when the container restarts. -

-

- The default path /var/lib/zerobyte/repositories is - already mounted from the host and is safe to use. -

-
-
- - Cancel - { - setShowPathBrowser(true); - setShowPathWarning(false); - }} - > - I Understand, Continue - - -
-
- - - - - Select Repository Directory - - Choose a directory from the filesystem to store the repository. - - -
- form.setValue("path", path)} - selectedPath={form.watch("path") || "/var/lib/zerobyte/repositories"} - /> -
- - - - Cancel - - setShowPathBrowser(false)}> - - Done - - -
-
- - )} - - {watchedBackend === "s3" && ( - <> - ( - - Endpoint - - - - S3-compatible endpoint URL. - - - )} - /> - ( - - Bucket - - - - S3 bucket name for storing backups. - - - )} - /> - ( - - Access Key ID - - - - S3 access key ID for authentication. - - - )} - /> - ( - - Secret Access Key - - - - S3 secret access key for authentication. - - - )} - /> - - )} - - {watchedBackend === "r2" && ( - <> - ( - - Endpoint - - - - - R2 endpoint (without https://). Find in R2 dashboard under bucket settings. - - - - )} - /> - ( - - Bucket - - - - R2 bucket name for storing backups. - - - )} - /> - ( - - Access Key ID - - - - R2 API token Access Key ID (create in Cloudflare R2 dashboard). - - - )} - /> - ( - - Secret Access Key - - - - R2 API token Secret Access Key (shown once when creating token). - - - )} - /> - - )} - - {watchedBackend === "gcs" && ( - <> - ( - - Bucket - - - - GCS bucket name for storing backups. - - - )} - /> - ( - - Project ID - - - - Google Cloud project ID. - - - )} - /> - ( - - Service Account JSON - - - - Service account JSON credentials for authentication. - - - )} - /> - - )} - - {watchedBackend === "azure" && ( - <> - ( - - Container - - - - Azure Blob Storage container name for storing backups. - - - )} - /> - ( - - Account Name - - - - Azure Storage account name. - - - )} - /> - ( - - Account Key - - - - Azure Storage account key for authentication. - - - )} - /> - ( - - Endpoint Suffix (Optional) - - - - Custom Azure endpoint suffix (defaults to core.windows.net). - - - )} - /> - - )} - - {watchedBackend === "rclone" && - (!rcloneRemotes || rcloneRemotes.length === 0 ? ( - - -

No rclone remotes configured

-

- To use rclone, you need to configure remotes on your host system -

- - View rclone documentation - - -
-
- ) : ( - <> - ( - - Remote - - Select the rclone remote configured on your host system. - - - )} - /> - ( - - Path - - - - Path within the remote where backups will be stored. - - - )} - /> - - ))} - - {watchedBackend === "rest" && ( - <> - ( - - REST Server URL - - - - URL of the REST server. - - - )} - /> - ( - - Repository Path (Optional) - - - - Path to the repository on the REST server (leave empty for root). - - - )} - /> - ( - - Username (Optional) - - - - Username for REST server authentication. - - - )} - /> - ( - - Password (Optional) - - - - Password for REST server authentication. - - - )} - /> - - )} - - {watchedBackend === "sftp" && ( - <> - ( - - Host - - - - SFTP server hostname or IP address. - - - )} - /> - ( - - Port - - field.onChange(parseInt(e.target.value, 10))} - /> - - SSH port (default: 22). - - - )} - /> - ( - - User - - - - SSH username for authentication. - - - )} - /> - ( - - Path - - - - Repository path on the SFTP server. - - - )} - /> - ( - - SSH Private Key - -