Working on some bugs.

This commit is contained in:
charlesgauthereau
2025-07-27 21:26:31 +02:00
parent f9dc15ba73
commit 85047e7509
107 changed files with 971 additions and 2203 deletions
@@ -0,0 +1,96 @@
import { useState } from "react";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Input } from "@/components/ui/input";
import { isValidCronPart } from "@/utils/cron";
export const AdvancedCronSelect = ({
id,
label,
options,
type,
value,
defaultValue,
onValueChange,
}: {
id: string;
label: string;
options: string[];
type: string;
value: string;
defaultValue: string;
onValueChange: (value: string) => void;
}) => {
const [isAdvanced, setIsAdvanced] = useState(false);
const [customValue, setCustomValue] = useState(defaultValue || value);
const [error, setError] = useState<string | null>(null);
const handleBlur = () => {
if (customValue.trim() === "") {
setIsAdvanced(false);
} else if (!isValidCronPart(type, customValue)) {
setError("Invalid cron part value.");
} else {
setError(null);
onValueChange(customValue);
}
};
return (
<div className="grid grid-cols-2 items-center gap-2">
<Label htmlFor={id} className="text-left">
{label}
</Label>
{!isAdvanced ? (
<Select
id={id}
className="col-span-4"
value={defaultValue}
onValueChange={(value: string) => {
if (value === "advanced") {
setIsAdvanced(true);
} else {
setCustomValue(value);
onValueChange(value);
}
}}
>
<SelectTrigger>
<SelectValue>
{value}
{/*{options.includes(value) ? value : "Custom value"}*/}
</SelectValue>
{/*<SelectValue placeholder="Select value" />*/}
</SelectTrigger>
<SelectContent>
{options.map((opt: string) => (
<SelectItem key={opt} value={opt}>
{opt}
</SelectItem>
))}
<SelectItem value="advanced">Advanced</SelectItem>
</SelectContent>
</Select>
) : (
<Input
id={id}
className="col-span-4"
type="text"
value={customValue}
onChange={(e) => {
const newValue = e.target.value;
setCustomValue(newValue);
if (isValidCronPart(type, newValue)) {
setError(null);
} else {
setError("Invalid cron part value.");
}
}}
onBlur={handleBlur}
placeholder="e.g., *, 1-5, */5"
/>
)}
{error && <p className="text-sm text-red-500 col-span-4">{error}</p>}
</div>
);
};
@@ -0,0 +1,71 @@
"use client";
import { Clock9 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { CronInput } from "@/components/wrappers/dashboard/database/cron-button/cron-input";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
import { updateDatabaseBackupPolicyAction } from "@/components/wrappers/dashboard/database/cron-button/cron.action";
import {Database} from "@/db/schema/06_database";
export type CronButtonProps = {
database: Database;
};
export const CronButton = (props: CronButtonProps) => {
const router = useRouter();
const [isSwitched, setIsSwitched] = useState(props.database.backupPolicy !== null);
const updateDatabaseBackupPolicy = useMutation({
mutationFn: (value: string) => updateDatabaseBackupPolicyAction({ databaseId: props.database.id, backupPolicy: value }),
onSuccess: () => {
toast.success(`Method updated successfully.`);
router.refresh();
},
onError: () => {
toast.error(`An error occurred while updating backup method.`);
},
});
const handleTypeChange = async (state: boolean) => {
setIsSwitched(state);
if (state == false) {
await updateDatabaseBackupPolicy.mutateAsync("");
}
};
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" {...props}>
<Clock9 />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Backup method</DialogTitle>
<DialogDescription>Your settings for the backup method</DialogDescription>
</DialogHeader>
<Separator />
<h1>Select your backup method</h1>
<div className="flex items-center space-x-2">
<Label>Manual / Automatic </Label>
<Switch
checked={isSwitched}
onCheckedChange={async () => {
await handleTypeChange(!isSwitched);
}}
id="type-mode"
/>
</div>
{isSwitched ? <CronInput database={props.database} /> : null}
</DialogContent>
</Dialog>
);
};
@@ -0,0 +1,117 @@
import { AdvancedCronSelect } from "./advanced-cron-select";
import { updateDatabaseBackupPolicyAction } from "@/components/wrappers/dashboard/database/cron-button/cron.action";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import {Database} from "@/db/schema/06_database";
export type CronInputProps = {
database: Database;
};
export const CronInput = ({ database }: CronInputProps) => {
const [cron, setCron] = useState<string>(database.backupPolicy ?? "* * * * *");
const router = useRouter();
const updateBackupPolicy = useMutation({
mutationFn: (value: string) => updateDatabaseBackupPolicyAction({ databaseId: database.id, backupPolicy: value }),
onSuccess: () => {
toast.success(`Cron updated successfully.`);
router.refresh();
},
onError: () => {
toast.error(`An error occurred while updating cron value.`);
},
});
const handleChangeCron = (type: "minute" | "hour" | "day-of-month" | "month" | "day-of-week", value: string) => {
const cronParts = cron.split(" ");
const indexMap = { minute: 0, hour: 1, "day-of-month": 2, month: 3, "day-of-week": 4 };
cronParts[indexMap[type]] = value;
setCron(cronParts.join(" "));
};
const handleUpdateCron = async (cron: string) => {
await updateBackupPolicy.mutateAsync(cron);
};
return (
<>
<h1>Configure your cron schedule</h1>
<AdvancedCronSelect
id="minute"
label="Minute"
options={Array.from({ length: 60 }, (_, i) => String(i).padStart(2, "0"))}
type="minute"
value={cron.split(" ")[0]}
defaultValue={cron.split(" ")[0]}
onValueChange={(value) => handleChangeCron("minute", value)}
/>
<AdvancedCronSelect
id="hour"
label="Hour"
options={Array.from({ length: 24 }, (_, i) => String(i).padStart(2, "0"))}
type="hour"
value={cron.split(" ")[1]}
defaultValue={cron.split(" ")[1]}
onValueChange={(value) => handleChangeCron("hour", value)}
/>
<AdvancedCronSelect
id="day-of-month"
label="Day of Month"
options={Array.from({ length: 31 }, (_, i) => String(i + 1).padStart(2, "0"))}
type="day-of-month"
value={cron.split(" ")[2]}
defaultValue={cron.split(" ")[2]}
onValueChange={(value) => handleChangeCron("day-of-month", value)}
/>
<AdvancedCronSelect
id="month"
label="Month"
options={["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]}
type="month"
value={cron.split(" ")[3]}
defaultValue={cron.split(" ")[3]}
onValueChange={(value) => handleChangeCron("month", value)}
/>
<AdvancedCronSelect
id="day-of-week"
label="Day of Week"
options={["0", "1", "2", "3", "4", "5", "6"]}
type="day-of-week"
value={cron.split(" ")[4]}
defaultValue={cron.split(" ")[4]}
onValueChange={(value) => handleChangeCron("day-of-week", value)}
/>
<Separator />
<div className="grid gap-2">
<div className="flex items-center justify-between">
<div className="font-semibold">Cron Expression</div>
<div className="font-mono text-muted-foreground">{cron}</div>
</div>
<div className="text-sm text-muted-foreground">This cron expression determines when the job will run.</div>
</div>
<div className="flex justify-between gap-2">
<Button
onClick={async () => {
setCron("* * * * *");
await handleUpdateCron("* * * * *");
}}
variant="destructive"
>
Reset
</Button>
<Button
onClick={async () => {
await handleUpdateCron(cron);
}}
>
Save cron
</Button>
</div>
</>
);
};
@@ -0,0 +1,31 @@
"use server";
import { userAction } from "@/safe-actions";
import { z } from "zod";
import { db } from "@/db";
import { eq } from "drizzle-orm";
import * as drizzleDb from "@/db";
export const updateDatabaseBackupPolicyAction = userAction
.schema(
z.object({
databaseId: z.string(),
backupPolicy: z.string(),
})
)
.action(async ({ parsedInput }) => {
const cronPolicy = parsedInput.backupPolicy === "" ? null : parsedInput.backupPolicy;
const [updated] = await db
.update(drizzleDb.schemas.database)
.set({
backupPolicy: cronPolicy,
})
.where(eq(drizzleDb.schemas.database.id, parsedInput.databaseId))
.returning()
.execute();
return {
data: updated,
};
});