diff --git a/apps/dokploy/components/dashboard/project/add-import.tsx b/apps/dokploy/components/dashboard/project/add-import.tsx new file mode 100644 index 000000000..cdf1acbe4 --- /dev/null +++ b/apps/dokploy/components/dashboard/project/add-import.tsx @@ -0,0 +1,489 @@ +import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; +import { Code2, FileInput, Globe2, HardDrive, HelpCircle } from "lucide-react"; +import { 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 { CodeEditor } from "@/components/shared/code-editor"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + 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 { ScrollArea } from "@/components/ui/scroll-area"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +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"; +import { APP_NAME_MESSAGE, APP_NAME_REGEX } from "@/utils/schema"; + +const AddImportSchema = z.object({ + name: z.string().min(1, { message: "Name is required" }), + appName: z + .string() + .min(1, { message: "App name is required" }) + .regex(APP_NAME_REGEX, { message: APP_NAME_MESSAGE }), + base64: z.string().min(1, { message: "Base64 content is required" }), + serverId: z.string().optional(), +}); + +type AddImport = z.infer; + +type TemplateInfo = { + compose: string; + template: { + domains: Array<{ + serviceName: string; + port: number; + path?: string; + host?: string; + }>; + envs: string[]; + mounts: Array<{ filePath: string; content: string }>; + }; +}; + +interface Props { + environmentId: string; + projectName?: string; +} + +export const AddImport = ({ environmentId, projectName }: Props) => { + const utils = api.useUtils(); + const [visible, setVisible] = useState(false); + const [previewOpen, setPreviewOpen] = useState(false); + const [mountOpen, setMountOpen] = useState(false); + const [selectedMount, setSelectedMount] = useState<{ + filePath: string; + content: string; + } | null>(null); + const [templateInfo, setTemplateInfo] = useState(null); + + const slug = slugify(projectName); + const { data: isCloud } = api.settings.isCloud.useQuery(); + const { data: servers } = api.server.withSSHKey.useQuery(); + const shouldShowServerDropdown = !!(servers && servers.length > 0); + + const { mutateAsync: previewTemplate, isPending: isProcessing } = + api.compose.previewTemplate.useMutation(); + const { mutateAsync: createCompose, isPending: isCreating } = + api.compose.create.useMutation(); + const { mutateAsync: importCompose, isPending: isImporting } = + api.compose.import.useMutation(); + + const form = useForm({ + defaultValues: { name: "", appName: `${slug}-`, base64: "" }, + resolver: zodResolver(AddImportSchema), + }); + + const resetAll = () => { + form.reset({ name: "", appName: `${slug}-`, base64: "" }); + setTemplateInfo(null); + setPreviewOpen(false); + setMountOpen(false); + setSelectedMount(null); + }; + + const handleOpenChange = (open: boolean) => { + if (!open) resetAll(); + setVisible(open); + }; + + const handleLoad = async (data: AddImport) => { + try { + const result = await previewTemplate({ + appName: data.appName, + base64: data.base64.trim(), + serverId: data.serverId === "dokploy" ? undefined : data.serverId, + }); + setTemplateInfo(result); + setPreviewOpen(true); + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Error processing template", + ); + } + }; + + const handleImport = async () => { + const data = form.getValues(); + try { + const compose = await createCompose({ + name: data.name, + appName: data.appName, + environmentId, + composeType: "docker-compose", + serverId: data.serverId === "dokploy" ? undefined : data.serverId, + }); + await importCompose({ + composeId: compose.composeId, + base64: data.base64.trim(), + }); + toast.success("Compose imported successfully"); + await utils.environment.one.invalidate({ environmentId }); + resetAll(); + setVisible(false); + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Error importing compose", + ); + } + }; + + const handleCancelPreview = () => { + setPreviewOpen(false); + setTemplateInfo(null); + }; + + return ( + <> + + + e.preventDefault()} + > + + Import + + + + + Import Compose + + Paste a base64-encoded compose export to preview and import it + + + +
+ + ( + + Name + + { + const val = e.target.value || ""; + form.setValue( + "appName", + `${slug}-${slugify(val.trim())}`, + ); + field.onChange(val); + }} + /> + + + + )} + /> + + {shouldShowServerDropdown && ( + ( + + + + + + Select a Server{" "} + {!isCloud ? "(Optional)" : ""} + + + + + + If no server is selected, the compose will be + deployed on the server where the user is logged in. + + + + + + + + )} + /> + )} + + ( + + App Name + + + + + + )} + /> + + ( + + Configuration (Base64) + +