feat: add docker registry upload

This commit is contained in:
Mauricio Siu
2024-05-13 01:18:27 -06:00
parent e9245cee2c
commit 6c792564ae
41 changed files with 18267 additions and 1072 deletions

View File

@@ -0,0 +1,199 @@
import React from "react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { z } from "zod";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { toast } from "sonner";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import Link from "next/link";
import { Server } from "lucide-react";
interface Props {
applicationId: string;
}
const AddRedirectchema = z.object({
replicas: z.number(),
registryId: z.string(),
});
type AddCommand = z.infer<typeof AddRedirectchema>;
export const ShowClusterSettings = ({ applicationId }: Props) => {
const { data } = api.application.one.useQuery(
{
applicationId,
},
{ enabled: !!applicationId },
);
const { data: registries } = api.registry.all.useQuery();
const utils = api.useUtils();
const { mutateAsync, isLoading } = api.application.update.useMutation();
const form = useForm<AddCommand>({
defaultValues: {
registryId: data?.registryId || "",
replicas: data?.replicas || 1,
},
resolver: zodResolver(AddRedirectchema),
});
useEffect(() => {
if (data?.command) {
form.reset({
registryId: data?.registryId || "",
replicas: data?.replicas || 1,
});
}
}, [form, form.reset, form.formState.isSubmitSuccessful, data?.command]);
const onSubmit = async (data: AddCommand) => {
await mutateAsync({
applicationId,
registryId: data?.registryId,
replicas: data?.replicas,
})
.then(async () => {
toast.success("Command Updated");
await utils.application.one.invalidate({
applicationId,
});
})
.catch(() => {
toast.error("Error to update the command");
});
};
return (
<Card className="bg-background">
<CardHeader className="flex flex-row justify-between">
<div>
<CardTitle className="text-xl">Cluster Settings</CardTitle>
<CardDescription>
Add the registry and the replicas of the application
</CardDescription>
</div>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4"
>
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="replicas"
render={({ field }) => (
<FormItem>
<FormLabel>Replicas</FormLabel>
<FormControl>
<Input
placeholder="1"
{...field}
onChange={(e) => {
field.onChange(Number(e.target.value));
}}
type="number"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{registries && registries?.length === 0 ? (
<div className="pt-10">
<div className="flex flex-col items-center gap-3">
<Server className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
To use a cluster feature, you need to configure at least a
registry first. Please, go to{" "}
<Link
href="/dashboard/settings/cluster"
className="text-foreground"
>
Settings
</Link>{" "}
to do so.
</span>
</div>
</div>
) : (
<>
<FormField
control={form.control}
name="registryId"
render={({ field }) => (
<FormItem>
<FormLabel>Select a registry</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a container" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{registries?.map((registry) => (
<SelectItem
key={registry.registryId}
value={registry.registryId}
>
{registry.registryName}
</SelectItem>
))}
<SelectLabel>
Registries ({registries?.length})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</FormItem>
)}
/>
</>
)}
<div className="flex justify-end">
<Button isLoading={isLoading} type="submit" className="w-fit">
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
);
};

View File

@@ -23,6 +23,7 @@ import { AlertTriangle, Container } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const AddRegistrySchema = z.object({
@@ -38,6 +39,7 @@ const AddRegistrySchema = z.object({
registryUrl: z.string().min(1, {
message: "Registry URL is required",
}),
imagePrefix: z.string(),
});
type AddRegistry = z.infer<typeof AddRegistrySchema>;
@@ -45,39 +47,55 @@ type AddRegistry = z.infer<typeof AddRegistrySchema>;
export const AddRegistry = () => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, error, isError } = api.project.create.useMutation();
const { mutateAsync, error, isError } = api.registry.create.useMutation();
const { mutateAsync: testRegistry, isLoading } =
api.registry.testRegistry.useMutation();
const router = useRouter();
const form = useForm<AddRegistry>({
defaultValues: {
username: "",
password: "",
registryUrl: "",
imagePrefix: "",
registryName: "",
},
resolver: zodResolver(AddRegistrySchema),
});
console.log(form.formState.errors);
const password = form.watch("password");
const username = form.watch("username");
const registryUrl = form.watch("registryUrl");
const registryName = form.watch("registryName");
const imagePrefix = form.watch("imagePrefix");
useEffect(() => {
form.reset({
username: "",
password: "",
registryUrl: "",
imagePrefix: "",
});
}, [form, form.reset, form.formState.isSubmitSuccessful]);
const onSubmit = async (data: AddRegistry) => {
// await mutateAsync({
// name: data.name,
// description: data.description,
// })
// .then(async (data) => {
// await utils.project.all.invalidate();
// toast.success("Project Created");
// setIsOpen(false);
// router.push(`/dashboard/project/${data.projectId}`);
// })
// .catch(() => {
// toast.error("Error to create a project");
// });
await mutateAsync({
password: data.password,
registryName: data.registryName,
username: data.username,
registryUrl: data.registryUrl,
registryType: "cloud",
imagePrefix: data.imagePrefix,
})
.then(async (data) => {
await utils.registry.all.invalidate();
toast.success("Registry added");
setIsOpen(false);
})
.catch(() => {
toast.error("Error to add a registry");
});
};
return (
@@ -160,6 +178,22 @@ export const AddRegistry = () => {
)}
/>
</div>
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="imagePrefix"
render={({ field }) => (
<FormItem>
<FormLabel>Image Prefix</FormLabel>
<FormControl>
<Input {...field} placeholder="Image Prefix" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex flex-col gap-4">
<FormField
control={form.control}
@@ -169,9 +203,8 @@ export const AddRegistry = () => {
<FormLabel>Registry URL</FormLabel>
<FormControl>
<Input
placeholder="https://aws_account_id.dkr.ecr.us-west-2.amazonaws.c"
placeholder="https://aws_account_id.dkr.ecr.us-west-2.amazonaws.com"
{...field}
type="password"
/>
</FormControl>
@@ -180,7 +213,34 @@ export const AddRegistry = () => {
)}
/>
</div>
<DialogFooter>
<DialogFooter className="flex flex-row w-full sm:justify-between gap-4 flex-wrap">
<Button
type="button"
variant={"secondary"}
isLoading={isLoading}
onClick={async () => {
if (!form.formState.isValid) {
toast.error("Please fill all the fields");
return;
}
await testRegistry({
username: username,
password: password,
registryUrl: registryUrl,
registryName: registryName,
registryType: "cloud",
imagePrefix: imagePrefix,
}).then((data) => {
if (data) {
toast.success("Registry Tested Successfully");
} else {
toast.error("Registry Test Failed");
}
});
}}
>
Test Registry
</Button>
<Button isLoading={form.formState.isSubmitting} type="submit">
Create
</Button>

View File

@@ -0,0 +1,179 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
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, Container } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const AddRegistrySchema = z.object({
username: z.string().min(1, {
message: "Username is required",
}),
password: z.string().min(1, {
message: "Password is required",
}),
registryUrl: z.string().min(1, {
message: "Registry URL is required",
}),
});
type AddRegistry = z.infer<typeof AddRegistrySchema>;
export const AddSelfHostedRegistry = () => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, error, isError, isLoading } =
api.registry.enableSelfHostedRegistry.useMutation();
const router = useRouter();
const form = useForm<AddRegistry>({
defaultValues: {
username: "siumauricio",
password: "Password123",
registryUrl: "https://registry.dokploy.com",
},
resolver: zodResolver(AddRegistrySchema),
});
useEffect(() => {
form.reset({
registryUrl: "https://registry.dokploy.com",
username: "siumauricio",
password: "Password123",
});
}, [form, form.reset, form.formState.isSubmitSuccessful]);
const onSubmit = async (data: AddRegistry) => {
await mutateAsync({
registryUrl: data.registryUrl,
username: data.username,
password: data.password,
})
.then(async (data) => {
await utils.registry.all.invalidate();
toast.success("Self Hosted Registry Created");
setIsOpen(false);
})
.catch(() => {
toast.error("Error to create a self hosted registry");
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button>
<Container className="h-4 w-4" />
Enable Self Hosted Registry
</Button>
</DialogTrigger>
<DialogContent className="sm:m:max-w-lg ">
<DialogHeader>
<DialogTitle>Add a self hosted registry</DialogTitle>
<DialogDescription>
Fill the next fields to add a self hosted registry.
</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
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4"
>
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Username" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
placeholder="Password"
{...field}
type="password"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="registryUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Registry URL</FormLabel>
<FormControl>
<Input
placeholder="https://registry.dokploy.com"
{...field}
/>
</FormControl>
<FormDescription>
Point a DNS record to the VPS IP address.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<DialogFooter>
<Button isLoading={isLoading} type="submit">
Create
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -12,35 +12,44 @@ import {
} 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";
export const AddSelfRegistry = () => {
interface Props {
registryId: string;
}
export const DeleteRegistry = ({ registryId }: Props) => {
const { mutateAsync, isLoading } = api.registry.remove.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button>Enable Self Hosted Registry</Button>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground " />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This will setup a self hosted registry.
This action cannot be undone. This will permanently delete the
registry.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
// await mutateAsync({
// authId,
// })
// .then(async () => {
// utils.user.all.invalidate();
// toast.success("User delete succesfully");
// })
// .catch(() => {
// toast.error("Error to delete the user");
// });
await mutateAsync({
registryId,
})
.then(async () => {
utils.registry.all.invalidate();
toast.success("Registry deleted");
})
.catch(() => {
toast.error("Error to delete the registry");
});
}}
>
Confirm

View File

@@ -6,19 +6,36 @@ import {
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { Server, ShieldCheck } from "lucide-react";
import { Server } from "lucide-react";
import { AddRegistry } from "./add-docker-registry";
import { AddSelfRegistry } from "./add-self-registry";
import { AddSelfHostedRegistry } from "./add-self-docker-registry";
import { DeleteRegistry } from "./delete-registry";
export const ShowRegistry = () => {
const { data } = api.certificates.all.useQuery();
const { data } = api.registry.all.useQuery();
const haveSelfHostedRegistry = data?.some(
(registry) => registry.registryType === "selfHosted",
);
return (
<div className="h-full">
<Card className="bg-transparent h-full">
<CardHeader>
<CardTitle className="text-xl">Clusters</CardTitle>
<CardDescription>Add cluster to your application.</CardDescription>
<CardHeader className="flex flex-row gap-2 justify-between w-full items-center">
<div className="flex flex-col gap-2">
<CardTitle className="text-xl">Clusters</CardTitle>
<CardDescription>Add cluster to your application.</CardDescription>
</div>
<div className="flex flex-row gap-2">
{data && data?.length > 0 && (
<>
{!haveSelfHostedRegistry && <AddSelfHostedRegistry />}
<AddRegistry />
</>
)}
</div>
</CardHeader>
<CardContent className="space-y-2 pt-4 h-full">
{data?.length === 0 ? (
@@ -29,29 +46,28 @@ export const ShowRegistry = () => {
</span>
<div className="flex flex-row gap-2">
<AddSelfRegistry />
<AddSelfHostedRegistry />
<AddRegistry />
</div>
{/* <AddCertificate /> */}
</div>
) : (
<div className="flex flex-col gap-6">
{data?.map((destination, index) => (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{data?.map((registry, index) => (
<div
key={destination.certificateId}
className="flex items-center justify-between"
key={registry.registryId}
className="flex items-center justify-between border p-4 rounded-lg hover:bg-muted cursor-pointer"
>
<span className="text-sm text-muted-foreground">
{index + 1}. {destination.name}
{index + 1}. {registry.registryName}
</span>
<div className="flex flex-row gap-3">
{/* <DeleteCertificate
certificateId={destination.certificateId}
/> */}
<DeleteRegistry registryId={registry.registryId} />
</div>
</div>
))}
<div>{/* <AddCertificate /> */}</div>
</div>
)}