mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
fix: cron.ts (#365)
Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
This commit is contained in:
co-authored by
charles-gauthereau
parent
5e4a430049
commit
3e8a6d5006
@@ -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
@@ -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);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user