mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-09 07:55:25 +02:00
- Modified the onChange event for input fields in AddApplication, AddCompose, and AddDatabase components to ensure proper trimming of whitespace from the input value before slugification.
319 lines
8.9 KiB
TypeScript
319 lines
8.9 KiB
TypeScript
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { CircuitBoard, HelpCircle } from "lucide-react";
|
|
import { useEffect, useState } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { toast } from "sonner";
|
|
import { z } from "zod";
|
|
import { AlertBlock } from "@/components/shared/alert-block";
|
|
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 {
|
|
Select,
|
|
SelectContent,
|
|
SelectGroup,
|
|
SelectItem,
|
|
SelectLabel,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip";
|
|
import { slugify } from "@/lib/slug";
|
|
import { api } from "@/utils/api";
|
|
|
|
const AddComposeSchema = z.object({
|
|
composeType: z.enum(["docker-compose", "stack"]).optional(),
|
|
name: z.string().min(1, {
|
|
message: "Name is required",
|
|
}),
|
|
appName: z
|
|
.string()
|
|
.min(1, {
|
|
message: "App name is required",
|
|
})
|
|
.regex(/^[a-z](?!.*--)([a-z0-9-]*[a-z])?$/, {
|
|
message:
|
|
"App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
|
}),
|
|
description: z.string().optional(),
|
|
serverId: z.string().optional(),
|
|
});
|
|
|
|
type AddCompose = z.infer<typeof AddComposeSchema>;
|
|
|
|
interface Props {
|
|
environmentId: string;
|
|
projectName?: string;
|
|
}
|
|
|
|
export const AddCompose = ({ environmentId, projectName }: Props) => {
|
|
const utils = api.useUtils();
|
|
const [visible, setVisible] = useState(false);
|
|
const slug = slugify(projectName);
|
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
|
const { mutateAsync, isLoading, error, isError } =
|
|
api.compose.create.useMutation();
|
|
|
|
// Get environment data to extract projectId
|
|
const { data: environment } = api.environment.one.useQuery({ environmentId });
|
|
|
|
const hasServers = servers && servers.length > 0;
|
|
// Show dropdown logic based on cloud environment
|
|
// Cloud: show only if there are remote servers (no Dokploy option)
|
|
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
|
|
const shouldShowServerDropdown = hasServers;
|
|
|
|
const form = useForm<AddCompose>({
|
|
defaultValues: {
|
|
name: "",
|
|
description: "",
|
|
composeType: "docker-compose",
|
|
appName: `${slug}-`,
|
|
},
|
|
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,
|
|
environmentId,
|
|
composeType: data.composeType,
|
|
appName: data.appName,
|
|
serverId: data.serverId === "dokploy" ? undefined : data.serverId,
|
|
})
|
|
.then(async () => {
|
|
toast.success("Compose Created");
|
|
setVisible(false);
|
|
// Invalidate the project query to refresh the environment data
|
|
await utils.environment.one.invalidate({
|
|
environmentId,
|
|
});
|
|
})
|
|
.catch(() => {
|
|
toast.error("Error creating the compose");
|
|
});
|
|
};
|
|
|
|
return (
|
|
<Dialog open={visible} onOpenChange={setVisible}>
|
|
<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="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}
|
|
onChange={(e) => {
|
|
const val = e.target.value || "";
|
|
const serviceName = slugify(val.trim());
|
|
form.setValue("appName", `${slug}-${serviceName}`);
|
|
field.onChange(val);
|
|
}}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
{shouldShowServerDropdown && (
|
|
<FormField
|
|
control={form.control}
|
|
name="serverId"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<TooltipProvider delayDuration={0}>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
|
Select a Server {!isCloud ? "(Optional)" : ""}
|
|
<HelpCircle className="size-4 text-muted-foreground" />
|
|
</FormLabel>
|
|
</TooltipTrigger>
|
|
<TooltipContent
|
|
className="z-[999] w-[300px]"
|
|
align="start"
|
|
side="top"
|
|
>
|
|
<span>
|
|
If no server is selected, the application will be
|
|
deployed on the server where the user is logged in.
|
|
</span>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
|
|
<Select
|
|
onValueChange={field.onChange}
|
|
defaultValue={
|
|
field.value || (!isCloud ? "dokploy" : undefined)
|
|
}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue
|
|
placeholder={!isCloud ? "Dokploy" : "Select a Server"}
|
|
/>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectGroup>
|
|
{!isCloud && (
|
|
<SelectItem value="dokploy">
|
|
<span className="flex items-center gap-2 justify-between w-full">
|
|
<span>Dokploy</span>
|
|
<span className="text-muted-foreground text-xs self-center">
|
|
Default
|
|
</span>
|
|
</span>
|
|
</SelectItem>
|
|
)}
|
|
{servers?.map((server) => (
|
|
<SelectItem
|
|
key={server.serverId}
|
|
value={server.serverId}
|
|
>
|
|
<span className="flex items-center gap-2 justify-between w-full">
|
|
<span>{server.name}</span>
|
|
<span className="text-muted-foreground text-xs self-center">
|
|
{server.ipAddress}
|
|
</span>
|
|
</span>
|
|
</SelectItem>
|
|
))}
|
|
<SelectLabel>
|
|
Servers ({servers?.length + (!isCloud ? 1 : 0)})
|
|
</SelectLabel>
|
|
</SelectGroup>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
)}
|
|
<FormField
|
|
control={form.control}
|
|
name="appName"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>App Name</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="my-app" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<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 of 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>
|
|
);
|
|
};
|