diff --git a/app/client/modules/backups/components/create-schedule-form.tsx b/app/client/modules/backups/components/create-schedule-form.tsx index 56457b93..4ae1faf5 100644 --- a/app/client/modules/backups/components/create-schedule-form.tsx +++ b/app/client/modules/backups/components/create-schedule-form.tsx @@ -1,4 +1,5 @@ import { arktypeResolver } from "@hookform/resolvers/arktype"; + import { useQuery } from "@tanstack/react-query"; import { type } from "arktype"; import { useCallback, useState } from "react"; @@ -18,6 +19,7 @@ import { } from "~/client/components/ui/form"; import { Input } from "~/client/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select"; +import { Button } from "~/client/components/ui/button"; import { Textarea } from "~/client/components/ui/textarea"; import { VolumeFileBrowser } from "~/client/components/volume-file-browser"; import type { BackupSchedule, Volume } from "~/client/lib/types"; @@ -33,6 +35,7 @@ const internalFormSchema = type({ frequency: "string", dailyTime: "string?", weeklyDay: "string?", + monthlyDays: "string[]?", keepLast: "number?", keepHourly: "number?", keepDaily: "number?", @@ -78,15 +81,19 @@ const backupScheduleToFormValues = (schedule?: BackupSchedule): InternalFormValu } const parts = schedule.cronExpression.split(" "); - const [minutePart, hourPart, , , dayOfWeekPart] = parts; + const [minutePart, hourPart, dayOfMonthPart, , dayOfWeekPart] = parts; const isHourly = hourPart === "*"; - const isDaily = !isHourly && dayOfWeekPart === "*"; - const frequency = isHourly ? "hourly" : isDaily ? "daily" : "weekly"; + const isMonthly = !isHourly && dayOfMonthPart !== "*" && dayOfWeekPart === "*"; + const isDaily = !isHourly && dayOfMonthPart === "*" && dayOfWeekPart === "*"; + + const frequency = isHourly ? "hourly" : isMonthly ? "monthly" : isDaily ? "daily" : "weekly"; const dailyTime = isHourly ? undefined : `${hourPart.padStart(2, "0")}:${minutePart.padStart(2, "0")}`; const weeklyDay = frequency === "weekly" ? dayOfWeekPart : undefined; + + const monthlyDays = isMonthly ? dayOfMonthPart.split(",") : undefined; const patterns = schedule.includePatterns || []; const isGlobPattern = (p: string) => /[*?[\]]/.test(p); @@ -96,7 +103,8 @@ const backupScheduleToFormValues = (schedule?: BackupSchedule): InternalFormValu return { name: schedule.name, repositoryId: schedule.repositoryId, - frequency, + frequency, + monthlyDays, dailyTime, weeklyDay, includePatterns: fileBrowserPaths.length > 0 ? fileBrowserPaths : undefined, @@ -249,6 +257,7 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }: Hourly Daily Weekly + Monthly @@ -302,6 +311,42 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }: )} /> )} + {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. + +
+ )} + /> + )} diff --git a/app/client/modules/backups/routes/backup-details.tsx b/app/client/modules/backups/routes/backup-details.tsx index 9ddecf1f..dd67883c 100644 --- a/app/client/modules/backups/routes/backup-details.tsx +++ b/app/client/modules/backups/routes/backup-details.tsx @@ -141,7 +141,7 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon const handleSubmit = (formValues: BackupScheduleFormValues) => { if (!schedule) return; - const cronExpression = getCronExpression(formValues.frequency, formValues.dailyTime, formValues.weeklyDay); + const cronExpression = getCronExpression(formValues.frequency, formValues.dailyTime, formValues.weeklyDay, formValues.monthlyDays); const retentionPolicy: Record = {}; if (formValues.keepLast) retentionPolicy.keepLast = formValues.keepLast; diff --git a/app/client/modules/backups/routes/create-backup.tsx b/app/client/modules/backups/routes/create-backup.tsx index f0592b67..4c51ac34 100644 --- a/app/client/modules/backups/routes/create-backup.tsx +++ b/app/client/modules/backups/routes/create-backup.tsx @@ -71,7 +71,7 @@ export default function CreateBackup({ loaderData }: Route.ComponentProps) { const handleSubmit = (formValues: BackupScheduleFormValues) => { if (!selectedVolumeId) return; - const cronExpression = getCronExpression(formValues.frequency, formValues.dailyTime, formValues.weeklyDay); + const cronExpression = getCronExpression(formValues.frequency, formValues.dailyTime, formValues.weeklyDay, formValues.monthlyDays); const retentionPolicy: Record = {}; if (formValues.keepLast) retentionPolicy.keepLast = formValues.keepLast; diff --git a/app/utils/utils.ts b/app/utils/utils.ts index dc5d3a29..ba382152 100644 --- a/app/utils/utils.ts +++ b/app/utils/utils.ts @@ -1,6 +1,6 @@ import { intervalToDuration } from "date-fns"; -export const getCronExpression = (frequency: string, dailyTime?: string, weeklyDay?: string): string => { +export const getCronExpression = (frequency: string, dailyTime?: string, weeklyDay?: string,monthlyDays?: string[]): string => { if (frequency === "hourly") { return "0 * * * *"; } @@ -14,6 +14,12 @@ export const getCronExpression = (frequency: string, dailyTime?: string, weeklyD if (frequency === "daily") { return `${minutes} ${hours} * * *`; } + + if (frequency === "monthly") { + const sortedDays = (monthlyDays || []).map(Number).sort((a, b) => a - b); + const days = sortedDays.length > 0 ? sortedDays.join(",") : "1"; + return `${minutes} ${hours} ${days} * *`; + } return `${minutes} ${hours} * * ${weeklyDay ?? "0"}`; };