Merge branch 'alexzapd-montly-schedule2'

This commit is contained in:
Nicolas Meienberger 2025-12-21 20:38:08 +01:00
commit f952532290
4 changed files with 73 additions and 7 deletions

View file

@ -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,16 @@ 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);
@ -97,6 +101,7 @@ const backupScheduleToFormValues = (schedule?: BackupSchedule): InternalFormValu
name: schedule.name,
repositoryId: schedule.repositoryId,
frequency,
monthlyDays,
dailyTime,
weeklyDay,
includePatterns: fileBrowserPaths.length > 0 ? fileBrowserPaths : undefined,
@ -249,6 +254,7 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
<SelectItem value="hourly">Hourly</SelectItem>
<SelectItem value="daily">Daily</SelectItem>
<SelectItem value="weekly">Weekly</SelectItem>
<SelectItem value="monthly">Specific days</SelectItem>
</SelectContent>
</Select>
</FormControl>
@ -302,6 +308,42 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
)}
/>
)}
{frequency === "monthly" && (
<FormField
control={form.control}
name="monthlyDays"
render={({ field }) => (
<FormItem className="md:col-span-2">
<FormLabel>Days of the month</FormLabel>
<FormControl>
<div className="grid grid-cols-7 gap-4 w-max">
{Array.from({ length: 31 }, (_, i) => {
const day = (i + 1).toString();
const isSelected = field.value?.includes(day);
return (
<Button
type="button"
key={day}
variant={isSelected ? "primary" : "secondary"}
size="icon"
onClick={() => {
const current = field.value || [];
const next = isSelected ? current.filter((d) => d !== day) : [...current, day];
field.onChange(next);
}}
>
{day}
</Button>
);
})}
</div>
</FormControl>
<FormDescription>Select one or more days when the backup should run.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
</CardContent>
</Card>

View file

@ -141,7 +141,12 @@ 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<string, number> = {};
if (formValues.keepLast) retentionPolicy.keepLast = formValues.keepLast;

View file

@ -71,7 +71,12 @@ 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<string, number> = {};
if (formValues.keepLast) retentionPolicy.keepLast = formValues.keepLast;

View file

@ -1,6 +1,11 @@
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 * * * *";
}
@ -15,6 +20,15 @@ export const getCronExpression = (frequency: string, dailyTime?: string, weeklyD
return `${minutes} ${hours} * * *`;
}
if (frequency === "monthly") {
const sortedDays = (monthlyDays || [])
.map(Number)
.filter((day) => day >= 1 && day <= 31)
.sort((a, b) => a - b);
const days = sortedDays.length > 0 ? sortedDays.join(",") : "1";
return `${minutes} ${hours} ${days} * *`;
}
return `${minutes} ${hours} * * ${weeklyDay ?? "0"}`;
};