import { BookText, CheckIcon, ChevronsUpDown, Globe, HelpCircle, LayoutGrid, List, Loader2, PuzzleIcon, SearchIcon, } from "lucide-react"; import Link from "next/link"; import { useEffect, useState } from "react"; import { toast } from "sonner"; import { GithubIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, } from "@/components/ui/command"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; const TEMPLATE_BASE_URL_KEY = "dokploy_template_base_url"; interface Props { environmentId: string; baseUrl?: string; } export const AddTemplate = ({ environmentId, baseUrl }: Props) => { const [query, setQuery] = useState(""); const [open, setOpen] = useState(false); const [viewMode, setViewMode] = useState<"detailed" | "icon">("detailed"); const [selectedTags, setSelectedTags] = useState([]); const [customBaseUrl, setCustomBaseUrl] = useState(() => { // Try to get from props first, then localStorage if (baseUrl) return baseUrl; if (typeof window !== "undefined") { return localStorage.getItem(TEMPLATE_BASE_URL_KEY) || undefined; } return undefined; }); // Get environment data to extract projectId const { data: environment } = api.environment.one.useQuery({ environmentId }); // Save to localStorage when customBaseUrl changes useEffect(() => { if (customBaseUrl) { localStorage.setItem(TEMPLATE_BASE_URL_KEY, customBaseUrl); } else { localStorage.removeItem(TEMPLATE_BASE_URL_KEY); } }, [customBaseUrl]); const { data, isLoading: isLoadingTemplates, error: errorTemplates, isError: isErrorTemplates, } = api.compose.templates.useQuery( { baseUrl: customBaseUrl }, { enabled: open, }, ); const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: servers } = api.server.withSSHKey.useQuery(); const { data: tags, isLoading: isLoadingTags } = api.compose.getTags.useQuery( { baseUrl: customBaseUrl }, { enabled: open, }, ); const utils = api.useUtils(); const [serverId, setServerId] = useState(undefined); const { mutateAsync, isLoading, error, isError } = api.compose.deployTemplate.useMutation(); const templates = data?.filter((template) => { const matchesTags = selectedTags.length === 0 || template.tags.some((tag) => selectedTags.includes(tag)); const matchesQuery = query === "" || template.name.toLowerCase().includes(query.toLowerCase()) || template.description.toLowerCase().includes(query.toLowerCase()); return matchesTags && matchesQuery; }) || []; 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; return ( e.preventDefault()} > Template
Create from Template Create an open source application from a template
setQuery(e.target.value)} className="w-full" value={query} /> setCustomBaseUrl(e.target.value || undefined) } className="w-full sm:w-[300px]" value={customBaseUrl || ""} /> {isLoadingTags && ( Loading Tags.... )} No tags found. {tags?.map((tag) => ( { if (selectedTags.includes(tag)) { setSelectedTags( selectedTags.filter((t) => t !== tag), ); return; } setSelectedTags([...selectedTags, tag]); }} > {tag} ))}
{selectedTags.length > 0 && (
{selectedTags.map((tag) => ( setSelectedTags(selectedTags.filter((t) => t !== tag)) } > {tag} × ))}
)}
{isError && ( {error?.message} )} {isErrorTemplates && ( {errorTemplates?.message} )} {isLoadingTemplates ? (
Loading templates...
) : templates.length === 0 ? (
No templates found
) : (
{templates?.map((template, idx) => (
{template?.version}
{template?.name}
{template?.name} {viewMode === "detailed" && template?.tags?.length > 0 && (
{template?.tags?.map((tag) => ( {tag} ))}
)}
{/* Template Content */} {viewMode === "detailed" && (
{template?.description}
)} {/* Create Button */}
{viewMode === "detailed" && (
{template?.links?.github && ( )} {template?.links?.website && ( )} {template?.links?.docs && ( )}
)} Are you absolutely sure? This will create an application from the{" "} {template?.name} template and add it to your project. {shouldShowServerDropdown && (
If no server is selected, the application will be deployed on the server where the user is logged in.
)}
Cancel { const promise = mutateAsync({ serverId: serverId === "dokploy" ? undefined : serverId, environmentId, id: template.id, baseUrl: customBaseUrl, }); toast.promise(promise, { loading: "Setting up...", success: () => { // Invalidate the project query to refresh the environment data utils.environment.one.invalidate({ environmentId, }); setOpen(false); return `${template.name} template created successfully`; }, error: () => { return `An error occurred deploying ${template.name} template`; }, }); }} > Confirm
))}
)}
); };