fix: cron.ts (#365)

Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
This commit is contained in:
Charles GTE
2026-07-09 09:36:44 +02:00
committed by GitHub
co-authored by charles-gauthereau
parent 5e4a430049
commit 3e8a6d5006
3 changed files with 79 additions and 18 deletions
@@ -1,4 +1,4 @@
import { useState } from "react"; import { useEffect, useState } from "react";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -12,6 +12,7 @@ export const AdvancedCronSelect = ({
value, value,
defaultValue, defaultValue,
onValueChange, onValueChange,
onValidityChange,
}: { }: {
id: string; id: string;
label: string; label: string;
@@ -20,11 +21,21 @@ export const AdvancedCronSelect = ({
value: string; value: string;
defaultValue: string; defaultValue: string;
onValueChange: (value: string) => void; onValueChange: (value: string) => void;
onValidityChange?: (valid: boolean) => void;
}) => { }) => {
const [isAdvanced, setIsAdvanced] = useState(false); const [isAdvanced, setIsAdvanced] = useState(false);
const [customValue, setCustomValue] = useState(defaultValue || value); const [customValue, setCustomValue] = useState(value || defaultValue);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => {
setCustomValue(value || defaultValue);
}, [value, defaultValue]);
useEffect(() => {
const valid = !isAdvanced || isValidCronPart(type, customValue);
onValidityChange?.(valid);
}, [isAdvanced, customValue, type, onValidityChange]);
const handleBlur = () => { const handleBlur = () => {
if (customValue.trim() === "") { if (customValue.trim() === "") {
setIsAdvanced(false); setIsAdvanced(false);
@@ -46,7 +57,7 @@ export const AdvancedCronSelect = ({
// @ts-ignore // @ts-ignore
id={id} id={id}
className="col-span-4" className="col-span-4"
value={defaultValue} value={value}
onValueChange={(value: string) => { onValueChange={(value: string) => {
if (value === "advanced") { if (value === "advanced") {
setIsAdvanced(true); setIsAdvanced(true);
@@ -2,7 +2,7 @@ import {AdvancedCronSelect} from "@/features/database/components/cron-advanced-s
import {updateDatabaseBackupPolicyAction} from "@/features/database/actions/cron.action"; import {updateDatabaseBackupPolicyAction} from "@/features/database/actions/cron.action";
import {useMutation, useQueryClient} from "@tanstack/react-query"; import {useMutation, useQueryClient} from "@tanstack/react-query";
import {useRouter} from "next/navigation"; import {useRouter} from "next/navigation";
import {useState} from "react"; import {useCallback, useState} from "react";
import {toast} from "sonner"; import {toast} from "sonner";
import {Button} from "@/components/ui/button"; import {Button} from "@/components/ui/button";
import {Separator} from "@/components/ui/separator"; import {Separator} from "@/components/ui/separator";
@@ -14,10 +14,17 @@ export type CronInputProps = {
}; };
export const CronInput = ({database, onSuccess}: CronInputProps) => { export const CronInput = ({database, onSuccess}: CronInputProps) => {
const [cron, setCron] = useState<string>(database.backupPolicy ?? "* * * * *"); const [cron, setCron] = useState<string>(database.backupPolicy ?? "0 0 * * *");
const [fieldValidity, setFieldValidity] = useState<Record<string, boolean>>({});
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
const setFieldValid = useCallback((id: string) => (valid: boolean) => {
setFieldValidity((prev) => (prev[id] === valid ? prev : {...prev, [id]: valid}));
}, []);
const hasInvalidField = Object.values(fieldValidity).some((valid) => !valid);
const updateBackupPolicy = useMutation({ const updateBackupPolicy = useMutation({
mutationFn: (value: string) => updateDatabaseBackupPolicyAction({databaseId: database.id, backupPolicy: value}), mutationFn: (value: string) => updateDatabaseBackupPolicyAction({databaseId: database.id, backupPolicy: value}),
onSuccess: () => { onSuccess: () => {
@@ -48,38 +55,42 @@ export const CronInput = ({database, onSuccess}: CronInputProps) => {
<AdvancedCronSelect <AdvancedCronSelect
id="minute" id="minute"
label="Minute" label="Minute"
options={Array.from({length: 60}, (_, i) => String(i).padStart(2, "0"))} options={Array.from({length: 60}, (_, i) => String(i))}
type="minute" type="minute"
value={cron.split(" ")[0]} value={cron.split(" ")[0]}
defaultValue={cron.split(" ")[0]} defaultValue={cron.split(" ")[0]}
onValueChange={(value) => handleChangeCron("minute", value)} onValueChange={(value) => handleChangeCron("minute", value)}
onValidityChange={setFieldValid("minute")}
/> />
<AdvancedCronSelect <AdvancedCronSelect
id="hour" id="hour"
label="Hour" label="Hour"
options={Array.from({length: 24}, (_, i) => String(i).padStart(2, "0"))} options={Array.from({length: 24}, (_, i) => String(i))}
type="hour" type="hour"
value={cron.split(" ")[1]} value={cron.split(" ")[1]}
defaultValue={cron.split(" ")[1]} defaultValue={cron.split(" ")[1]}
onValueChange={(value) => handleChangeCron("hour", value)} onValueChange={(value) => handleChangeCron("hour", value)}
onValidityChange={setFieldValid("hour")}
/> />
<AdvancedCronSelect <AdvancedCronSelect
id="day-of-month" id="day-of-month"
label="Day of Month" label="Day of Month"
options={Array.from({length: 31}, (_, i) => String(i + 1).padStart(2, "0"))} options={Array.from({length: 31}, (_, i) => String(i + 1))}
type="day-of-month" type="day-of-month"
value={cron.split(" ")[2]} value={cron.split(" ")[2]}
defaultValue={cron.split(" ")[2]} defaultValue={cron.split(" ")[2]}
onValueChange={(value) => handleChangeCron("day-of-month", value)} onValueChange={(value) => handleChangeCron("day-of-month", value)}
onValidityChange={setFieldValid("day-of-month")}
/> />
<AdvancedCronSelect <AdvancedCronSelect
id="month" id="month"
label="Month" label="Month"
options={["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]} options={Array.from({length: 12}, (_, i) => String(i + 1))}
type="month" type="month"
value={cron.split(" ")[3]} value={cron.split(" ")[3]}
defaultValue={cron.split(" ")[3]} defaultValue={cron.split(" ")[3]}
onValueChange={(value) => handleChangeCron("month", value)} onValueChange={(value) => handleChangeCron("month", value)}
onValidityChange={setFieldValid("month")}
/> />
<AdvancedCronSelect <AdvancedCronSelect
id="day-of-week" id="day-of-week"
@@ -89,6 +100,7 @@ export const CronInput = ({database, onSuccess}: CronInputProps) => {
value={cron.split(" ")[4]} value={cron.split(" ")[4]}
defaultValue={cron.split(" ")[4]} defaultValue={cron.split(" ")[4]}
onValueChange={(value) => handleChangeCron("day-of-week", value)} onValueChange={(value) => handleChangeCron("day-of-week", value)}
onValidityChange={setFieldValid("day-of-week")}
/> />
<Separator/> <Separator/>
<div className="grid gap-2"> <div className="grid gap-2">
@@ -113,6 +125,7 @@ export const CronInput = ({database, onSuccess}: CronInputProps) => {
onClick={async () => { onClick={async () => {
await handleUpdateCron(cron); await handleUpdateCron(cron);
}} }}
disabled={hasInvalidField || updateBackupPolicy.isPending}
> >
Save cron Save cron
</Button> </Button>
+45 -8
View File
@@ -1,10 +1,47 @@
const cronBounds: Record<string, [number, number]> = {
minute: [0, 59],
hour: [0, 23],
"day-of-month": [1, 31],
month: [1, 12],
"day-of-week": [0, 6],
};
const isNumberInRange = (value: string, min: number, max: number): boolean => {
if (!/^\d+$/.test(value)) return false;
const n = Number(value);
return n >= min && n <= max;
};
const isRangeOrNumber = (value: string, min: number, max: number): boolean => {
const range = value.match(/^(\d+)-(\d+)$/);
if (range) {
const start = Number(range[1]);
const end = Number(range[2]);
return isNumberInRange(range[1], min, max) && isNumberInRange(range[2], min, max) && start <= end;
}
return isNumberInRange(value, min, max);
};
export const isValidCronPart = (type: string, value: string): boolean => { export const isValidCronPart = (type: string, value: string): boolean => {
const regexMap: Record<string, RegExp> = { const bounds = cronBounds[type];
minute: /^(\*|([0-5]?\d)|(\d+(,\d+)*|(\d+-\d+)|(\*\/\d+)))$/, if (!bounds) return false;
hour: /^(\*|([01]?\d|2[0-3])|(\d+(,\d+)*|(\d+-\d+)|(\*\/\d+)))$/, const [min, max] = bounds;
"day-of-month": /^(\*|([1-9]|[12]\d|3[01])|(\d+(,\d+)*|(\d+-\d+)|(\*\/\d+)))$/,
month: /^(\*|([1-9]|1[0-2])|(\d+(,\d+)*|(\d+-\d+)|(\*\/\d+)))$/, return value.split(",").every((rawPart) => {
"day-of-week": /^(\*|[0-6]|(\d+(,\d+)*|(\d+-\d+)|(\*\/\d+)))$/, const part = rawPart.trim();
}; if (part === "") return false;
return regexMap[type]?.test(value) ?? false;
const step = part.match(/^(.+)\/(\d+)$/);
if (step) {
const base = step[1];
const stepValue = Number(step[2]);
if (!/^\d+$/.test(step[2]) || stepValue < 1 || stepValue > max) return false;
if (base === "*") return true;
return isRangeOrNumber(base, min, max);
}
if (part === "*") return true;
return isRangeOrNumber(part, min, max);
});
}; };