From 17b4027e0b6e322d09f8cdd4d0c41e5d41402e1f Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:18:14 +0100 Subject: [PATCH] refactor: split create schedule form (#453) --- .../components/create-schedule-form.tsx | 748 ------------------ .../basic-info-section.tsx | 70 ++ .../create-schedule-form/exclude-section.tsx | 85 ++ .../frequency-section.tsx | 137 ++++ .../components/create-schedule-form/index.tsx | 171 ++++ .../create-schedule-form/paths-section.tsx | 92 +++ .../retention-section.tsx | 140 ++++ .../create-schedule-form/summary-section.tsx | 112 +++ .../components/create-schedule-form/types.ts | 45 ++ .../components/create-schedule-form/utils.ts | 30 + .../repositories/routes/snapshot-details.tsx | 15 +- 11 files changed, 895 insertions(+), 750 deletions(-) delete mode 100644 app/client/modules/backups/components/create-schedule-form.tsx create mode 100644 app/client/modules/backups/components/create-schedule-form/basic-info-section.tsx create mode 100644 app/client/modules/backups/components/create-schedule-form/exclude-section.tsx create mode 100644 app/client/modules/backups/components/create-schedule-form/frequency-section.tsx create mode 100644 app/client/modules/backups/components/create-schedule-form/index.tsx create mode 100644 app/client/modules/backups/components/create-schedule-form/paths-section.tsx create mode 100644 app/client/modules/backups/components/create-schedule-form/retention-section.tsx create mode 100644 app/client/modules/backups/components/create-schedule-form/summary-section.tsx create mode 100644 app/client/modules/backups/components/create-schedule-form/types.ts create mode 100644 app/client/modules/backups/components/create-schedule-form/utils.ts diff --git a/app/client/modules/backups/components/create-schedule-form.tsx b/app/client/modules/backups/components/create-schedule-form.tsx deleted file mode 100644 index 718061f8..00000000 --- a/app/client/modules/backups/components/create-schedule-form.tsx +++ /dev/null @@ -1,748 +0,0 @@ -import { arktypeResolver } from "@hookform/resolvers/arktype"; - -import { useQuery } from "@tanstack/react-query"; -import { type } from "arktype"; -import { X } from "lucide-react"; -import { useCallback, useState } from "react"; -import { useForm } from "react-hook-form"; -import { listRepositoriesOptions } from "~/client/api-client/@tanstack/react-query.gen"; -import { CronInput } from "~/client/components/cron-input"; -import { RepositoryIcon } from "~/client/components/repository-icon"; -import { Button } from "~/client/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card"; -import { Checkbox } from "~/client/components/ui/checkbox"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "~/client/components/ui/form"; -import { Input } from "~/client/components/ui/input"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select"; -import { Textarea } from "~/client/components/ui/textarea"; -import { VolumeFileBrowser } from "~/client/components/volume-file-browser"; -import type { BackupSchedule, Volume } from "~/client/lib/types"; -import { deepClean } from "~/utils/object"; -import { cronToFormValues } from "../lib/cron-utils"; - -const internalFormSchema = type({ - name: "1 <= string <= 128", - repositoryId: "string", - excludePatternsText: "string?", - excludeIfPresentText: "string?", - includePatternsText: "string?", - includePatterns: "string[]?", - frequency: "string", - dailyTime: "string?", - weeklyDay: "string?", - monthlyDays: "string[]?", - cronExpression: "string?", - keepLast: "number?", - keepHourly: "number?", - keepDaily: "number?", - keepWeekly: "number?", - keepMonthly: "number?", - keepYearly: "number?", - oneFileSystem: "boolean?", -}); -const cleanSchema = type.pipe((d) => internalFormSchema(deepClean(d))); - -export const weeklyDays = [ - { label: "Monday", value: "1" }, - { label: "Tuesday", value: "2" }, - { label: "Wednesday", value: "3" }, - { label: "Thursday", value: "4" }, - { label: "Friday", value: "5" }, - { label: "Saturday", value: "6" }, - { label: "Sunday", value: "0" }, -]; - -type InternalFormValues = typeof internalFormSchema.infer; - -export type BackupScheduleFormValues = Omit< - InternalFormValues, - "excludePatternsText" | "excludeIfPresentText" | "includePatternsText" -> & { - excludePatterns?: string[]; - excludeIfPresent?: string[]; -}; - -type Props = { - volume: Volume; - initialValues?: BackupSchedule; - onSubmit: (data: BackupScheduleFormValues) => void; - loading?: boolean; - summaryContent?: React.ReactNode; - formId: string; -}; - -const backupScheduleToFormValues = (schedule?: BackupSchedule): InternalFormValues | undefined => { - if (!schedule) { - return undefined; - } - - const cronValues = cronToFormValues(schedule.cronExpression ?? "0 * * * *"); - - const patterns = schedule.includePatterns || []; - const isGlobPattern = (p: string) => /[*?[\]]/.test(p); - const fileBrowserPaths = patterns.filter((p) => !isGlobPattern(p)); - const textPatterns = patterns.filter(isGlobPattern); - - return { - name: schedule.name, - repositoryId: schedule.repositoryId, - includePatterns: fileBrowserPaths.length > 0 ? fileBrowserPaths : undefined, - includePatternsText: textPatterns.length > 0 ? textPatterns.join("\n") : undefined, - excludePatternsText: schedule.excludePatterns?.join("\n") || undefined, - excludeIfPresentText: schedule.excludeIfPresent?.join("\n") || undefined, - oneFileSystem: schedule.oneFileSystem ?? false, - ...cronValues, - ...schedule.retentionPolicy, - }; -}; - -export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }: Props) => { - const form = useForm({ - resolver: arktypeResolver(cleanSchema as unknown as typeof internalFormSchema), - defaultValues: backupScheduleToFormValues(initialValues), - }); - - const handleSubmit = useCallback( - (data: InternalFormValues) => { - const { - excludePatternsText, - excludeIfPresentText, - includePatternsText, - includePatterns: fileBrowserPatterns, - cronExpression, - ...rest - } = data; - const excludePatterns = excludePatternsText - ? excludePatternsText - .split("\n") - .map((p) => p.trim()) - .filter(Boolean) - : []; - - const excludeIfPresent = excludeIfPresentText - ? excludeIfPresentText - .split("\n") - .map((p) => p.trim()) - .filter(Boolean) - : []; - - const textPatterns = includePatternsText - ? includePatternsText - .split("\n") - .map((p) => p.trim()) - .filter(Boolean) - : []; - const includePatterns = [...(fileBrowserPatterns || []), ...textPatterns]; - - onSubmit({ - ...rest, - cronExpression, - includePatterns: includePatterns.length > 0 ? includePatterns : [], - excludePatterns, - excludeIfPresent, - }); - }, - [onSubmit], - ); - - const { data: repositoriesData } = useQuery({ - ...listRepositoriesOptions(), - }); - - const frequency = form.watch("frequency"); - const formValues = form.watch(); - - const [selectedPaths, setSelectedPaths] = useState>(new Set(initialValues?.includePatterns || [])); - - const handleSelectionChange = useCallback( - (paths: Set) => { - setSelectedPaths(paths); - form.setValue("includePatterns", Array.from(paths)); - }, - [form], - ); - - const handleRemovePath = useCallback( - (pathToRemove: string) => { - const newPaths = new Set(selectedPaths); - newPaths.delete(pathToRemove); - setSelectedPaths(newPaths); - form.setValue("includePatterns", Array.from(newPaths)); - }, - [selectedPaths, form], - ); - - return ( -
- -
- - - Backup automation - - Schedule automated backups of {volume.name} to a secure repository. - - - - ( - - Backup name - - - - A unique name to identify this backup schedule. - - - )} - /> - - ( - - Backup repository - - - - - Choose where encrypted backups for {volume.name} will be stored. - - - - )} - /> - - ( - - Backup frequency - - - - Define how often snapshots should be taken. - - - )} - /> - - {frequency === "cron" && ( - ( - - )} - /> - )} - - {frequency !== "hourly" && frequency !== "cron" && ( - ( - - Execution time - - - - Time of day when the backup will run. - - - )} - /> - )} - - {frequency === "weekly" && ( - ( - - Execution day - - - - Choose which day of the week to run the backup. - - - )} - /> - )} - {frequency === "monthly" && ( - ( - - Days of the month - -
- {Array.from({ length: 31 }, (_, i) => { - const day = (i + 1).toString(); - const isSelected = field.value?.includes(day); - return ( - - ); - })} -
-
- Select one or more days when the backup should run. - -
- )} - /> - )} -
-
- - - - Backup paths - - Select which folders or files to include in the backup. If no paths are selected, the entire volume will - be backed up. - - - - - {selectedPaths.size > 0 && ( -
-

Selected paths:

-
- {Array.from(selectedPaths).map((path) => ( - - {path} - - - ))} -
-
- )} - ( - - Additional include patterns - -