refactoring some files.

This commit is contained in:
killian-larcher
2024-12-16 11:55:16 +01:00
parent 5484e8841a
commit 317c043815
93 changed files with 207 additions and 265 deletions
@@ -0,0 +1,94 @@
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,95 @@
"use client"
import {Clock9} from "lucide-react";
import {Button} from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription, DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger
} from "@/components/ui/dialog";
import {CronInput} from "@/components/wrappers/dashboard/database/CronButton/CronInput";
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 {
updateStorageSettingsAction
} from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.action";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
import {Database} from "@prisma/client";
import {
updateBackupPolicyAction,
updateDatabaseBackupPolicyAction
} from "@/components/wrappers/dashboard/database/CronButton/cron.action";
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,123 @@
import { AdvancedCronSelect } from "./AdvancedCronSelect";
import {
updateBackupPolicyAction,
updateDatabaseBackupPolicyAction
} from "@/components/wrappers/dashboard/database/CronButton/cron.action";
import {useMutation} from "@tanstack/react-query";
import {useState} from "react";
import {useRouter} from "next/navigation";
import {toast} from "sonner";
import {Database} from "@prisma/client";
import {Button} from "@/components/ui/button";
import {Separator} from "@/components/ui/separator";
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: string, 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 {prisma} from "@/prisma";
export const updateDatabaseBackupPolicyAction = userAction
.schema(
z.object({
databaseId: z.string(),
backupPolicy: z.string(),
}
)
)
.action(async ({parsedInput, ctx}) => {
const cronPolicy = parsedInput.backupPolicy == "" ? null : parsedInput.backupPolicy
const updatedDatabase = await prisma.database.update({
where: {
id: parsedInput.databaseId,
},
data: {
backupPolicy: cronPolicy,
}
})
return {
data: updatedDatabase,
}
})
@@ -0,0 +1,119 @@
"use client";
import {Card, CardContent} from "@/components/ui/card";
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
useZodForm
} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {Form} from "@/components/ui/form"
import {Button} from "@/components/ui/button";
import {useRouter} from "next/navigation";
import {useMutation} from "@tanstack/react-query";
import {TooltipProvider} from "@/components/ui/tooltip";
import {DatabaseSchema, DatabaseType} from "@/components/wrappers/dashboard/database/DatabaseForm/form-database.schema";
import {updateDatabaseAction} from "@/components/wrappers/dashboard/database/DatabaseForm/form-database.action";
import {toast} from "sonner";
export type DatabaseFormProps = {
defaultValues?: DatabaseType;
databaseId?: string;
}
export const DatabaseForm = (props: DatabaseFormProps) => {
const {defaultValues, databaseId} = props;
const isCreate = !Boolean(defaultValues)
const form = useZodForm({
schema: DatabaseSchema,
defaultValues: {...defaultValues},
});
const router = useRouter();
const mutation = useMutation({
mutationFn: async (values: DatabaseType) => {
const database = await updateDatabaseAction({id: databaseId, data: values});
if (database.serverError) {
console.error(database?.serverError);
toast.error(database?.serverError);
return;
}
console.log(database)
toast.success(`Database settings successfully updated!`);
router.back()
}
})
return (
<TooltipProvider>
<Card>
<CardContent>
<Form form={form}
className="flex flex-col gap-4 mt-3"
onSubmit={async (values) => {
console.log("sssssss")
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="name"
render={({field}) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input disabled placeholder="Database 1" {...field}/>
</FormControl>
<FormDescription>Your database project name setup in agent</FormDescription>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="dbms"
render={({field}) => (
<FormItem>
<FormLabel>Database type</FormLabel>
<FormControl>
<Input disabled placeholder="PostgreSQL" {...field}/>
</FormControl>
<FormDescription>Your database project name setup in agent</FormDescription>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({field}) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input placeholder="Prod database for project 1" {...field}/>
</FormControl>
<FormDescription>Add a short description about this database</FormDescription>
<FormMessage/>
</FormItem>
)}
/>
<Button>
{isCreate ? `Create database` : `Save database`}
</Button>
</Form>
</CardContent>
</Card>
</TooltipProvider>
)
}
@@ -0,0 +1,25 @@
"use server"
import {z} from "zod";
import {userAction} from "@/safe-actions";
import {prisma} from "@/prisma";
import {DatabaseSchema} from "@/components/wrappers/dashboard/database/DatabaseForm/form-database.schema";
export const updateDatabaseAction = userAction
.schema(
z.object({
id: z.string(),
data: DatabaseSchema,
}
)
)
.action(async ({parsedInput, ctx}) => {
return prisma.database.update({
where: {
id: parsedInput.id,
},
data: parsedInput.data,
});
})
@@ -0,0 +1,12 @@
import {z} from "zod";
const cronRegex = /^(\d{1,2}|\*|(\d{1,2}-\d{1,2})|(\d{1,2}\/\d{1,2}))\s+(\d{1,2}|\*|(\d{1,2}-\d{1,2})|(\d{1,2}\/\d{1,2}))\s+(\d{1,2}|\*|(\d{1,2}-\d{1,2})|(\d{1,2}\/\d{1,2}))\s+(\d{1,7}|\*|(\d{1,7}-\d{1,7})|(\d{1,7}\/\d{1,7}))$/;
export const DatabaseSchema = z.object({
name: z.string().readonly(),
description: z.string().optional(),
dbms: z.string().readonly(),
});
export type DatabaseType = z.infer<typeof DatabaseSchema>;
@@ -0,0 +1,20 @@
"use client"
import {buttonVariants} from "@/components/ui/button";
import {GearIcon} from "@radix-ui/react-icons";
import Link from "next/link";
import {usePathname} from "next/navigation";
export type EditButtonProps = {}
export const EditButton = (props: EditButtonProps) => {
const pathname = usePathname();
return(
<Link
className={buttonVariants({ variant: "outline" })}
href={`${pathname}/edit`}
>
<GearIcon className="w-7 h-7" />
</Link>
)
}
@@ -0,0 +1,207 @@
"use client";
import {useState} from "react";
import {DateTimePicker} from "@/components/wrappers/daytime-picker";
import {Button} from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
useZodForm
} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {RestoreSchema} from "@/components/wrappers/dashboard/database/restore-form.schema";
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
import {useMutation} from "@tanstack/react-query";
import {ComboBox, ComboBoxFormItem} from "@/components/wrappers/common/combobox";
import {Label} from "@/components/ui/label";
export type restoreFormProps = {
databaseToRestore: any
databases: any[]
backups: any[]
}
export const RestoreForm = (props: restoreFormProps) => {
const {databaseToRestore, databases, backups} = props
console.log("bacups", backups)
console.log("bacups", databaseToRestore)
const backupLocations = [
{
value: "remote-file",
label: "Remote File",
},
{
value: "desktop-file",
label: "Desktop File",
},
]
const executionModes = [
{
value: "immediate",
label: "Immediate",
},
{
value: "scheduled",
label: "Scheduled",
},
]
const [selectedDatabaseId, setSelectedDatabaseId] = useState(databaseToRestore.id)
const filteredBackups = backups.filter(backup => backup.databaseId == selectedDatabaseId)
const defaultValues = {
executionMode: "immediate",
backupLocation: "remote-file",
}
const form = useZodForm({
schema: RestoreSchema,
defaultValues: defaultValues
});
const values = form.getValues()
console.log("values", values)
const mutation = useMutation({})
return (
<div>
<Form
form={form}
onSubmit={async (values) => {
console.log(values);
}}
>
<FormField
control={form.control}
name="backupLocation"
render={({field}) => (
<FormItem>
<FormLabel>Backup location</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue/>
</SelectTrigger>
</FormControl>
<SelectContent>
{backupLocations.map((backupLocation, key) =>
<SelectItem key={key} value={backupLocation.value}>
{backupLocation.label}
</SelectItem>
)}
</SelectContent>
</Select>
<FormMessage/>
</FormItem>
)}
/>
{values.backupLocation == "remote-file" ?
<>
<div className="flex flex-col">
<Label>Database</Label>
<ComboBox
values={databases.map(database =>
({"value": database.id, "label": database.name})
)}
onValueChange={setSelectedDatabaseId}
defaultValue={selectedDatabaseId}
searchField
/>
</div>
<FormField
control={form.control}
name="remoteBackup"
render={({field}) => (
<FormItem className="flex flex-col">
<FormLabel>Remote backup</FormLabel>
<ComboBoxFormItem
values={filteredBackups.map(backup => ({
"value": backup.id,
"label": backup.createdAt.toString()
})
)}
{...field}
/>
<FormMessage/>
</FormItem>
)}
/>
</>
: null}
{values.backupLocation == "desktop-file" ?
<FormField
control={form.control}
name="uploadedBackupFile"
render={({field}) => (
<FormItem>
<FormLabel>File</FormLabel>
<FormControl>
<Input id="uploadedBackupFile" type="file" {...field}/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/> : null}
<FormField
control={form.control}
name="executionMode"
render={({field}) => (
<FormItem>
<FormLabel>Execution mode</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue/>
</SelectTrigger>
</FormControl>
<SelectContent>
{executionModes.map((executionMode, key) =>
<SelectItem key={key} value={executionMode.value}>
{executionMode.label}
</SelectItem>
)}
</SelectContent>
</Select>
<FormMessage/>
</FormItem>
)}
/>
{values.executionMode == "scheduled" ?
<FormField
control={form.control}
name="scheduledDatetime"
render={({field}) => (
<FormItem>
<FormLabel>Scheduled date</FormLabel>
<FormControl>
<DateTimePicker value={field.value} onChange={field.onChange}/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/> : null}
<Button type="submit">Launch restore</Button>
</Form>
</div>
)
}
@@ -0,0 +1,24 @@
import {z} from "zod";
const ImmediateExecutionSchema = z.object({
executionMode: z.literal('immediate'),
});
const ScheduledExecutionSchema = z.object({
executionMode: z.literal('scheduled'),
scheduledDatetime: z.date(),
});
const CommonBackupSchema = z.union([ImmediateExecutionSchema, ScheduledExecutionSchema]);
const RemoteBackupSchema = z.object({
backupLocation: z.literal('remote-file'),
}).merge(CommonBackupSchema);
const DesktopBackupSchema = z.object({
backupLocation: z.literal('desktop-file'),
uploadedBackupFile: z.instanceof(File),
}).merge(CommonBackupSchema);
export const RestoreSchema = z.union([RemoteBackupSchema, DesktopBackupSchema]);
export type RestoreType = z.infer<typeof RestoreSchema>;