feat: initial commit

This commit is contained in:
Mauricio Siu
2024-04-28 23:57:52 -06:00
parent 8857a20344
commit be56ba046c
412 changed files with 60777 additions and 1 deletions

View File

@@ -0,0 +1,235 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const addDestination = z.object({
name: z.string().min(1, "Name is required"),
accessKeyId: z.string(),
secretAccessKey: z.string(),
bucket: z.string(),
region: z.string(),
endpoint: z.string(),
});
type AddDestination = z.infer<typeof addDestination>;
export const AddDestination = () => {
const utils = api.useUtils();
const { mutateAsync, isError, error, isLoading } =
api.destination.create.useMutation();
const { mutateAsync: testConnection, isLoading: isLoadingConnection } =
api.destination.testConnection.useMutation();
const form = useForm<AddDestination>({
defaultValues: {
accessKeyId: "",
bucket: "",
name: "",
region: "",
secretAccessKey: "",
endpoint: "",
},
resolver: zodResolver(addDestination),
});
useEffect(() => {
form.reset();
}, [form, form.reset, form.formState.isSubmitSuccessful]);
const onSubmit = async (data: AddDestination) => {
await mutateAsync({
accessKey: data.accessKeyId,
bucket: data.bucket,
endpoint: data.endpoint,
name: data.name,
region: data.region,
secretAccessKey: data.secretAccessKey,
})
.then(async () => {
toast.success("Destination Created");
await utils.destination.all.invalidate();
})
.catch(() => {
toast.error("Error to create the Destination");
});
};
return (
<Dialog>
<DialogTrigger className="" asChild>
<Button>Add Destination</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Add Destination</DialogTitle>
<DialogDescription>
In this section you can add destinations for your backups.
</DialogDescription>
</DialogHeader>
{isError && (
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
<AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400">
{error?.message}
</span>
</div>
)}
<Form {...form}>
<form
id="hook-form-destination-add"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 "
>
<FormField
control={form.control}
name="name"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder={"S3 Bucket"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="accessKeyId"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Access Key Id</FormLabel>
<FormControl>
<Input placeholder={"xcas41dasde"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="secretAccessKey"
render={({ field }) => (
<FormItem>
<div className="space-y-0.5">
<FormLabel>Secret Access Key</FormLabel>
</div>
<FormControl>
<Input placeholder={"asd123asdasw"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="bucket"
render={({ field }) => (
<FormItem>
<div className="space-y-0.5">
<FormLabel>Bucket</FormLabel>
</div>
<FormControl>
<Input placeholder={"dokploy-bucket"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="region"
render={({ field }) => (
<FormItem>
<div className="space-y-0.5">
<FormLabel>Region</FormLabel>
</div>
<FormControl>
<Input placeholder={"us-east-1"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="endpoint"
render={({ field }) => (
<FormItem>
<FormLabel>Endpoint</FormLabel>
<FormControl>
<Input
placeholder={"https://us.bucket.aws/s3"}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
<Button
isLoading={isLoadingConnection}
type="button"
variant="secondary"
onClick={async () => {
await testConnection({
accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"),
endpoint: form.getValues("endpoint"),
name: "Test",
region: form.getValues("region"),
secretAccessKey: form.getValues("secretAccessKey"),
})
.then(async () => {
toast.success("Connection Success");
})
.catch(() => {
toast.error("Error to connect the provider");
});
}}
>
Test connection
</Button>
<Button
isLoading={isLoading}
form="hook-form-destination-add"
type="submit"
>
Create
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,61 @@
import React from "react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { TrashIcon } from "lucide-react";
import { toast } from "sonner";
interface Props {
destinationId: string;
}
export const DeleteDestination = ({ destinationId }: Props) => {
const { mutateAsync, isLoading } = api.destination.remove.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the
destination
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
destinationId,
})
.then(() => {
utils.destination.all.invalidate();
toast.success("Destination delete succesfully");
})
.catch(() => {
toast.error("Error to delete destination");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -0,0 +1,65 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { FolderUp } from "lucide-react";
import { AddDestination } from "./add-destination";
import { UpdateDestination } from "./update-destination";
import { DeleteDestination } from "./delete-destination";
export const ShowDestinations = () => {
const { data } = api.destination.all.useQuery();
return (
<div className="w-full">
<Card className="h-full bg-transparent">
<CardHeader>
<CardTitle className="text-xl">S3 Destinations</CardTitle>
<CardDescription>
Add your providers like AWS S3, Cloudflare R2, Wasabi, DigitalOcean
Spaces etc.
</CardDescription>
</CardHeader>
<CardContent className="space-y-2 pt-4">
{data?.length === 0 ? (
<div className="flex flex-col items-center gap-3">
<FolderUp className="size-8 self-center text-muted-foreground" />
<span className="text-base text-muted-foreground">
To create a backup is required to set at least 1 provider.
</span>
<AddDestination />
</div>
) : (
<div className="flex flex-col gap-6">
{data?.map((destination, index) => (
<div
key={destination.destinationId}
className="flex items-center justify-between"
>
<span className="text-sm text-muted-foreground">
{index + 1}. {destination.name}
</span>
<div className="flex flex-row gap-3">
<UpdateDestination
destinationId={destination.destinationId}
/>
<DeleteDestination
destinationId={destination.destinationId}
/>
</div>
</div>
))}
<div>
<AddDestination />
</div>
</div>
)}
</CardContent>
</Card>
</div>
);
};

View File

@@ -0,0 +1,263 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, PenBoxIcon } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const updateDestination = z.object({
name: z.string().min(1, "Name is required"),
accessKeyId: z.string(),
secretAccessKey: z.string(),
bucket: z.string(),
region: z.string(),
endpoint: z.string(),
});
type UpdateDestination = z.infer<typeof updateDestination>;
interface Props {
destinationId: string;
}
export const UpdateDestination = ({ destinationId }: Props) => {
const utils = api.useUtils();
const { data, refetch } = api.destination.one.useQuery(
{
destinationId,
},
{
enabled: !!destinationId,
},
);
const { mutateAsync, isError, error } = api.destination.update.useMutation();
const { mutateAsync: testConnection, isLoading: isLoadingConnection } =
api.destination.testConnection.useMutation();
const form = useForm<UpdateDestination>({
defaultValues: {
accessKeyId: "",
bucket: "",
name: "",
region: "",
secretAccessKey: "",
endpoint: "",
},
resolver: zodResolver(updateDestination),
});
useEffect(() => {
if (data) {
form.reset({
accessKeyId: data.accessKey,
bucket: data.bucket,
endpoint: data.endpoint,
name: data.name,
region: data.region,
secretAccessKey: data.secretAccessKey,
});
}
}, [form, form.reset, data]);
const onSubmit = async (data: UpdateDestination) => {
await mutateAsync({
accessKey: data.accessKeyId,
bucket: data.bucket,
endpoint: data.endpoint,
name: data.name,
region: data.region,
secretAccessKey: data.secretAccessKey,
destinationId,
})
.then(async () => {
toast.success("Destination Updated");
await refetch();
await utils.destination.all.invalidate();
})
.catch(() => {
toast.error("Error to update the Destination");
});
};
return (
<Dialog>
<DialogTrigger className="" asChild>
<Button variant="ghost">
<PenBoxIcon className="size-4 text-muted-foreground" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Update Destination</DialogTitle>
<DialogDescription>
Update the current destination config
</DialogDescription>
</DialogHeader>
{isError && (
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
<AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400">
{error?.message}
</span>
</div>
)}
<Form {...form}>
<form
id="hook-form"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-8 "
>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<FormField
control={form.control}
name="name"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder={"S3 Bucket"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="accessKeyId"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Access Key Id</FormLabel>
<FormControl>
<Input placeholder={"xcas41dasde"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="secretAccessKey"
render={({ field }) => (
<FormItem>
<div className="space-y-0.5">
<FormLabel>Secret Access Key</FormLabel>
</div>
<FormControl>
<Input placeholder={"asd123asdasw"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="bucket"
render={({ field }) => (
<FormItem>
<div className="space-y-0.5">
<FormLabel>Bucket</FormLabel>
</div>
<FormControl>
<Input placeholder={"dokploy-bucket"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="region"
render={({ field }) => (
<FormItem>
<div className="space-y-0.5">
<FormLabel>Region</FormLabel>
</div>
<FormControl>
<Input placeholder={"us-east-1"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="endpoint"
render={({ field }) => (
<FormItem>
<FormLabel>Endpoint</FormLabel>
<FormControl>
<Input
placeholder={"https://us.bucket.aws/s3"}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
</form>
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
<Button
isLoading={isLoadingConnection}
type="button"
variant="secondary"
onClick={async () => {
await testConnection({
accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"),
endpoint: form.getValues("endpoint"),
name: "Test",
region: form.getValues("region"),
secretAccessKey: form.getValues("secretAccessKey"),
})
.then(async () => {
toast.success("Connection Success");
})
.catch(() => {
toast.error("Error to connect the provider");
});
}}
>
Test connection
</Button>
<Button
isLoading={form.formState.isSubmitting}
form="hook-form"
type="submit"
>
Update
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
};