Merge branch 'canary' into hehehai/feat-server-custom-name

This commit is contained in:
Mauricio Siu
2024-06-08 14:37:51 -06:00
295 changed files with 43807 additions and 3328 deletions

View File

@@ -18,9 +18,10 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { AlertBlock } from "@/components/shared/alert-block";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Folder } from "lucide-react";
import { Folder } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -104,14 +105,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
Assign a name and description to your application
</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>
)}
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form

View File

@@ -0,0 +1,186 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { AlertBlock } from "@/components/shared/alert-block";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CircuitBoard, Folder } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const AddComposeSchema = z.object({
composeType: z.enum(["docker-compose", "stack"]).optional(),
name: z.string().min(1, {
message: "Name is required",
}),
description: z.string().optional(),
});
type AddCompose = z.infer<typeof AddComposeSchema>;
interface Props {
projectId: string;
}
export const AddCompose = ({ projectId }: Props) => {
const utils = api.useUtils();
const { mutateAsync, isLoading, error, isError } =
api.compose.create.useMutation();
const form = useForm<AddCompose>({
defaultValues: {
name: "",
description: "",
composeType: "docker-compose",
},
resolver: zodResolver(AddComposeSchema),
});
useEffect(() => {
form.reset();
}, [form, form.reset, form.formState.isSubmitSuccessful]);
const onSubmit = async (data: AddCompose) => {
await mutateAsync({
name: data.name,
description: data.description,
projectId,
composeType: data.composeType,
})
.then(async () => {
toast.success("Compose Created");
await utils.project.one.invalidate({
projectId,
});
})
.catch(() => {
toast.error("Error to create the compose");
});
};
return (
<Dialog>
<DialogTrigger className="w-full">
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
onSelect={(e) => e.preventDefault()}
>
<CircuitBoard className="size-4 text-muted-foreground" />
<span>Compose</span>
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-xl">
<DialogHeader>
<DialogTitle>Create Compose</DialogTitle>
<DialogDescription>
Assign a name and description to your compose
</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4"
>
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Frontend" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="composeType"
render={({ field }) => (
<FormItem>
<FormLabel>Compose Type</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a compose type" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="docker-compose">
Docker Compose
</SelectItem>
<SelectItem value="stack">Stack</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Description about your service..."
className="resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
<DialogFooter>
<Button isLoading={isLoading} form="hook-form" type="submit">
Create
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,209 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { AlertBlock } from "@/components/shared/alert-block";
import { api } from "@/utils/api";
import { Code, Github, Globe, PuzzleIcon } from "lucide-react";
import Link from "next/link";
import { Input } from "@/components/ui/input";
import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { toast } from "sonner";
interface Props {
projectId: string;
}
export const AddTemplate = ({ projectId }: Props) => {
const [query, setQuery] = useState("");
const { data } = api.compose.templates.useQuery();
const utils = api.useUtils();
const { mutateAsync, isLoading, error, isError } =
api.compose.deployTemplate.useMutation();
const templates = data?.filter((t) =>
t.name.toLowerCase().includes(query.toLowerCase()),
);
return (
<Dialog>
<DialogTrigger className="w-full">
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
onSelect={(e) => e.preventDefault()}
>
<PuzzleIcon className="size-4 text-muted-foreground" />
<span>Templates</span>
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl p-0">
<div className="sticky top-0 z-10 flex flex-col gap-4 bg-black p-6 border-b">
<DialogHeader>
<DialogTitle>Create Template</DialogTitle>
<DialogDescription>
Deploy a open source template to your project
</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Input
placeholder="Search Template"
onChange={(e) => setQuery(e.target.value)}
value={query}
/>
</div>
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 w-full gap-4">
{templates?.map((template, index) => (
<div key={`template-${index}`}>
<div
key={template.id}
className="flex flex-col gap-4 border p-6 rounded-lg h-full"
>
<div className="flex flex-col gap-4">
<div className="flex flex-col items-center gap-2">
<img
src={`/templates/${template.logo}`}
className="size-28 object-contain"
alt=""
/>
</div>
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-2 justify-center items-center">
<div className="flex flex-col gap-2 items-center justify-center">
<div className="flex flex-row gap-2 flex-wrap">
<span className="text-sm font-medium">
{template.name}
</span>
<Badge>{template.version}</Badge>
</div>
<div className="flex flex-row gap-0">
<Link
href={template.links.github}
target="_blank"
className={
"text-sm text-muted-foreground p-3 rounded-full hover:bg-border items-center flex transition-colors"
}
>
<Github className="size-4 text-muted-foreground" />
</Link>
{template.links.website && (
<Link
href={template.links.website}
target="_blank"
className={
"text-sm text-muted-foreground p-3 rounded-full hover:bg-border items-center flex transition-colors"
}
>
<Globe className="size-4 text-muted-foreground" />
</Link>
)}
{template.links.docs && (
<Link
href={template.links.docs}
target="_blank"
className={
"text-sm text-muted-foreground p-3 rounded-full hover:bg-border items-center flex transition-colors"
}
>
<Globe className="size-4 text-muted-foreground" />
</Link>
)}
<Link
href={`https://github.com/dokploy/dokploy/tree/canary/templates/${template.id}`}
target="_blank"
className={
"text-sm text-muted-foreground p-3 rounded-full hover:bg-border items-center flex transition-colors"
}
>
<Code className="size-4 text-muted-foreground" />
</Link>
</div>
<div className="flex flex-row gap-2 flex-wrap justify-center">
{template.tags.map((tag) => (
<Badge variant="secondary" key={tag}>
{tag}
</Badge>
))}
</div>
</div>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button onSelect={(e) => e.preventDefault()}>
Deploy
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you absolutely sure?
</AlertDialogTitle>
<AlertDialogDescription>
This will deploy {template.name} template to
your project.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
projectId,
id: template.id,
})
.then(async () => {
toast.success(
`${template.name} template created succesfully`,
);
utils.project.one.invalidate({
projectId,
});
})
.catch(() => {
toast.error(
`Error to delete ${template.name} template`,
);
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
<p className="text-sm text-muted-foreground">
{template.description}
</p>
</div>
</div>
</div>
</div>
))}
</div>
</div>
</DialogContent>
</Dialog>
);
};