import { arktypeResolver } from "@hookform/resolvers/arktype"; import { useQuery } from "@tanstack/react-query"; import { type } from "arktype"; import { useCallback, useState } from "react"; import { useForm } from "react-hook-form"; import { listRepositoriesOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { RepositoryIcon } from "~/client/components/repository-icon"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card"; 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"; const internalFormSchema = type({ repositoryId: "string", excludePatternsText: "string?", includePatterns: "string[]?", frequency: "string", dailyTime: "string?", weeklyDay: "string?", keepLast: "number?", keepHourly: "number?", keepDaily: "number?", keepWeekly: "number?", keepMonthly: "number?", keepYearly: "number?", }); 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 & { excludePatterns?: 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 parts = schedule.cronExpression.split(" "); const [minutePart, hourPart, , , dayOfWeekPart] = parts; const isHourly = hourPart === "*"; const isDaily = !isHourly && dayOfWeekPart === "*"; const frequency = isHourly ? "hourly" : isDaily ? "daily" : "weekly"; const dailyTime = isHourly ? undefined : `${hourPart.padStart(2, "0")}:${minutePart.padStart(2, "0")}`; const weeklyDay = frequency === "weekly" ? dayOfWeekPart : undefined; return { repositoryId: schedule.repositoryId, frequency, dailyTime, weeklyDay, includePatterns: schedule.includePatterns || undefined, excludePatternsText: schedule.excludePatterns?.join("\n") || undefined, ...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) => { // Convert excludePatternsText string to excludePatterns array const { excludePatternsText, ...rest } = data; const excludePatterns = excludePatternsText ? excludePatternsText .split("\n") .map((p) => p.trim()) .filter(Boolean) : undefined; onSubmit({ ...rest, excludePatterns, }); }, [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], ); return (
Backup automation Schedule automated backups of {volume.name} to a secure repository. ( Backup repository Choose where encrypted backups for {volume.name} will be stored. )} /> ( Backup frequency Define how often snapshots should be taken. )} /> {frequency !== "hourly" && ( ( Execution time Time of day when the backup will run. )} /> )} {frequency === "weekly" && ( ( Execution day Choose which day of the week to run the backup. )} /> )} 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} ))}
)}
Exclude patterns Optionally specify patterns to exclude from backups. Enter one pattern per line (e.g., *.tmp, node_modules/**, .cache/). ( Exclusion patterns