mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Adding cron manager in front for database policy.
This commit is contained in:
@@ -59,7 +59,6 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
|
|||||||
</AgentModalKey>
|
</AgentModalKey>
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
<PageActions className="justify-between">
|
<PageActions className="justify-between">
|
||||||
<BackupButton/>
|
|
||||||
<Button>Restore</Button>
|
<Button>Restore</Button>
|
||||||
</PageActions>
|
</PageActions>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {AgentForm} from "@/components/wrappers/Agent/AgentForm/AgentForm";
|
||||||
|
import {requiredCurrentUser} from "@/auth/current-user";
|
||||||
|
import {prisma} from "@/prisma";
|
||||||
|
import {notFound} from "next/navigation";
|
||||||
|
import {DatabaseForm} from "@/components/wrappers/Database/DatabaseForm/DatabaseForm";
|
||||||
|
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{
|
||||||
|
databaseId: string;
|
||||||
|
}>) {
|
||||||
|
|
||||||
|
const {databaseId} = await props.params
|
||||||
|
|
||||||
|
const user = await requiredCurrentUser()
|
||||||
|
const database = await prisma.database.findUnique({
|
||||||
|
where: {
|
||||||
|
id: databaseId,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!database) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader>
|
||||||
|
<PageTitle>
|
||||||
|
Edit {database.name}
|
||||||
|
</PageTitle>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent>
|
||||||
|
<DatabaseForm databaseId={databaseId} defaultValues={database}/>
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {prisma} from "@/prisma";
|
import {prisma} from "@/prisma";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound, usePathname, useRouter} from "next/navigation";
|
||||||
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
||||||
import {BackupButton} from "@/components/wrappers/BackupButton/BackupButton";
|
import {BackupButton} from "@/components/wrappers/BackupButton/BackupButton";
|
||||||
import {DatabaseTabs} from "@/components/wrappers/Dashboard/Projects/Database/DatabaseTabs";
|
import {DatabaseTabs} from "@/components/wrappers/Dashboard/Projects/Database/DatabaseTabs";
|
||||||
import {DatabaseKpi} from "@/components/wrappers/Dashboard/Projects/Database/DatabaseKpi";
|
import {DatabaseKpi} from "@/components/wrappers/Dashboard/Projects/Database/DatabaseKpi";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {buttonVariants} from "@/components/ui/button";
|
||||||
|
import {GearIcon} from "@radix-ui/react-icons";
|
||||||
|
import {EditButton} from "@/components/wrappers/Database/EditButton/EditButton";
|
||||||
|
import {CronButton} from "@/components/wrappers/Database/CronButton/CronButton";
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ databaseId: string }>) {
|
export default async function RoutePage(props: PageParams<{ databaseId: string }>) {
|
||||||
@@ -57,6 +62,8 @@ export default async function RoutePage(props: PageParams<{ databaseId: string }
|
|||||||
<div className="justify-between gap-2 sm:flex">
|
<div className="justify-between gap-2 sm:flex">
|
||||||
<PageTitle className="flex items-center">
|
<PageTitle className="flex items-center">
|
||||||
{database.name}
|
{database.name}
|
||||||
|
<EditButton/>
|
||||||
|
<CronButton database={database}/>
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
<PageActions className="justify-between">
|
<PageActions className="justify-between">
|
||||||
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
|
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {useRouter} from "next/navigation";
|
|||||||
import {ButtonWithLoading} from "@/components/wrappers/Button/ButtonWithLoading/ButtonWithLoading";
|
import {ButtonWithLoading} from "@/components/wrappers/Button/ButtonWithLoading/ButtonWithLoading";
|
||||||
|
|
||||||
export type BackupButtonProps = {
|
export type BackupButtonProps = {
|
||||||
databaseId: string;
|
databaseId: string
|
||||||
disable: boolean
|
disable: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,92 @@
|
|||||||
|
"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/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/Settings/SettingsStorageTab/StorageS3Form/s3-form.action";
|
||||||
|
import {toast} from "sonner";
|
||||||
|
import {useRouter} from "next/navigation";
|
||||||
|
import {Database} from "@prisma/client";
|
||||||
|
import {updateBackupPolicyAction} from "@/components/wrappers/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 updateBackupPolicy = useMutation({
|
||||||
|
mutationFn: (value: string) => updateBackupPolicyAction({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 updateBackupPolicy.mutateAsync(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,120 @@
|
|||||||
|
import { AdvancedCronSelect } from "./AdvancedCronSelect";
|
||||||
|
import {updateBackupPolicyAction} from "@/components/wrappers/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) => updateBackupPolicyAction({ 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,29 @@
|
|||||||
|
"use server"
|
||||||
|
import {userAction} from "@/safe-actions";
|
||||||
|
import {z} from "zod";
|
||||||
|
import {prisma} from "@/prisma";
|
||||||
|
|
||||||
|
|
||||||
|
export const updateBackupPolicyAction = userAction
|
||||||
|
.schema(
|
||||||
|
z.object({
|
||||||
|
databaseId: z.string(),
|
||||||
|
backupPolicy: z.string(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.action(async ({parsedInput, ctx}) => {
|
||||||
|
const updatedDatabase = await prisma.database.update({
|
||||||
|
where: {
|
||||||
|
id: parsedInput.databaseId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
backupPolicy: parsedInput.backupPolicy,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
data: updatedDatabase,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
"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/Database/DatabaseForm/form-database.schema";
|
||||||
|
|
||||||
|
export type DatabaseFormProps = {
|
||||||
|
defaultValues?: DatabaseType;
|
||||||
|
databaseId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DatabaseForm = (props: DatabaseFormProps) => {
|
||||||
|
|
||||||
|
const isCreate = !Boolean(props.defaultValues)
|
||||||
|
|
||||||
|
const form = useZodForm({
|
||||||
|
schema: DatabaseSchema,
|
||||||
|
defaultValues: props.defaultValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: async (values: DatabaseType) => {
|
||||||
|
console.log("values", values)
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Form form={form}
|
||||||
|
className="flex flex-col gap-4 mt-3"
|
||||||
|
onSubmit={async (values) => {
|
||||||
|
await mutation.mutateAsync(values);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
disabled
|
||||||
|
defaultValue=""
|
||||||
|
render={({field}) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Database 1" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Your database project name setup in agent</FormDescription>
|
||||||
|
<FormMessage/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="dbms"
|
||||||
|
defaultValue=""
|
||||||
|
disabled
|
||||||
|
render={({field}) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Database type</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="PostgreSQL" {...field} value={field.value ?? ""} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Your database project name setup in agent</FormDescription>
|
||||||
|
<FormMessage/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="description"
|
||||||
|
defaultValue=""
|
||||||
|
render={({field}) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Description</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Prod database for project 1" {...field} value={field.value ?? ""} />
|
||||||
|
</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,13 @@
|
|||||||
|
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(),
|
||||||
|
description: z.string().optional().nullable(),
|
||||||
|
dbms: z.string(),
|
||||||
|
backupPolicy: z.string().regex(cronRegex, 'Invalid cron format')
|
||||||
|
});
|
||||||
|
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -50,7 +50,7 @@ export const backupColumns: ColumnDef<Backup>[] = [
|
|||||||
const {extendedProps} = table.options.meta;
|
const {extendedProps} = table.options.meta;
|
||||||
|
|
||||||
|
|
||||||
const isRestauring = !!rowData.restaurations.find(restauration => restauration.status === "waiting");
|
// const isRestauring = !!rowData.restaurations.find(restauration => restauration.status === "waiting");
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export const isValidCronPart = (type: string, value: string): boolean => {
|
||||||
|
const regexMap: Record<string, RegExp> = {
|
||||||
|
minute: /^(\*|([0-5]?\d)|(\d+(,\d+)*|(\d+-\d+)|(\*\/\d+)))$/,
|
||||||
|
hour: /^(\*|([01]?\d|2[0-3])|(\d+(,\d+)*|(\d+-\d+)|(\*\/\d+)))$/,
|
||||||
|
"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+)))$/,
|
||||||
|
"day-of-week": /^(\*|[0-6]|(\d+(,\d+)*|(\d+-\d+)|(\*\/\d+)))$/,
|
||||||
|
};
|
||||||
|
|
||||||
|
return regexMap[type]?.test(value) ?? false;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user