diff --git a/apps/dokploy/components/dashboard/project/add-ai-assistant.tsx b/apps/dokploy/components/dashboard/project/add-ai-assistant.tsx index 2bb47618e..88fd1d111 100644 --- a/apps/dokploy/components/dashboard/project/add-ai-assistant.tsx +++ b/apps/dokploy/components/dashboard/project/add-ai-assistant.tsx @@ -1,10 +1,10 @@ import { TemplateGenerator } from "@/components/dashboard/project/ai/template-generator"; interface Props { - projectId: string; + environmentId: string; projectName?: string; } -export const AddAiAssistant = ({ projectId }: Props) => { - return ; +export const AddAiAssistant = ({ environmentId }: Props) => { + return ; }; diff --git a/apps/dokploy/components/dashboard/project/add-application.tsx b/apps/dokploy/components/dashboard/project/add-application.tsx index 137f75a51..dd4effb0f 100644 --- a/apps/dokploy/components/dashboard/project/add-application.tsx +++ b/apps/dokploy/components/dashboard/project/add-application.tsx @@ -64,11 +64,11 @@ const AddTemplateSchema = z.object({ type AddTemplate = z.infer; interface Props { - projectId: string; + environmentId: string; projectName?: string; } -export const AddApplication = ({ projectId, projectName }: Props) => { +export const AddApplication = ({ environmentId, projectName }: Props) => { const utils = api.useUtils(); const { data: isCloud } = api.settings.isCloud.useQuery(); const [visible, setVisible] = useState(false); @@ -94,15 +94,15 @@ export const AddApplication = ({ projectId, projectName }: Props) => { name: data.name, appName: data.appName, description: data.description, - projectId, + environmentId, serverId: data.serverId, }) .then(async () => { toast.success("Service Created"); form.reset(); setVisible(false); - await utils.project.one.invalidate({ - projectId, + await utils.environment.one.invalidate({ + environmentId, }); }) .catch(() => { diff --git a/apps/dokploy/components/dashboard/project/add-compose.tsx b/apps/dokploy/components/dashboard/project/add-compose.tsx index c32e55c16..d565b5bfd 100644 --- a/apps/dokploy/components/dashboard/project/add-compose.tsx +++ b/apps/dokploy/components/dashboard/project/add-compose.tsx @@ -65,11 +65,11 @@ const AddComposeSchema = z.object({ type AddCompose = z.infer; interface Props { - projectId: string; + environmentId: string; projectName?: string; } -export const AddCompose = ({ projectId, projectName }: Props) => { +export const AddCompose = ({ environmentId, projectName }: Props) => { const utils = api.useUtils(); const [visible, setVisible] = useState(false); const slug = slugify(projectName); @@ -78,6 +78,9 @@ export const AddCompose = ({ projectId, projectName }: Props) => { 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; const form = useForm({ @@ -98,7 +101,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => { await mutateAsync({ name: data.name, description: data.description, - projectId, + environmentId, composeType: data.composeType, appName: data.appName, serverId: data.serverId, @@ -106,8 +109,9 @@ export const AddCompose = ({ projectId, projectName }: Props) => { .then(async () => { toast.success("Compose Created"); setVisible(false); - await utils.project.one.invalidate({ - projectId, + // Invalidate the project query to refresh the environment data + await utils.environment.one.invalidate({ + environmentId, }); }) .catch(() => { diff --git a/apps/dokploy/components/dashboard/project/add-database.tsx b/apps/dokploy/components/dashboard/project/add-database.tsx index 104413908..bda918998 100644 --- a/apps/dokploy/components/dashboard/project/add-database.tsx +++ b/apps/dokploy/components/dashboard/project/add-database.tsx @@ -170,11 +170,11 @@ const databasesMap = { type AddDatabase = z.infer; interface Props { - projectId: string; + environmentId: string; projectName?: string; } -export const AddDatabase = ({ projectId, projectName }: Props) => { +export const AddDatabase = ({ environmentId, projectName }: Props) => { const utils = api.useUtils(); const [visible, setVisible] = useState(false); const slug = slugify(projectName); @@ -185,6 +185,9 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { const mariadbMutation = api.mariadb.create.useMutation(); const mysqlMutation = api.mysql.create.useMutation(); + // Get environment data to extract projectId + const { data: environment } = api.environment.one.useQuery({ environmentId }); + const hasServers = servers && servers.length > 0; const form = useForm({ @@ -219,7 +222,8 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { name: data.name, appName: data.appName, dockerImage: defaultDockerImage, - projectId, + projectId: environment?.projectId || "", + environmentId, serverId: data.serverId, description: data.description, }; @@ -248,7 +252,6 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { ...commonParams, databasePassword: data.databasePassword, serverId: data.serverId, - projectId, }); } else if (data.type === "mariadb") { promise = mariadbMutation.mutateAsync({ @@ -287,8 +290,9 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { databaseUser: "", }); setVisible(false); - await utils.project.one.invalidate({ - projectId, + // Invalidate the project query to refresh the environment data + await utils.environment.one.invalidate({ + environmentId, }); }) .catch(() => { diff --git a/apps/dokploy/components/dashboard/project/add-template.tsx b/apps/dokploy/components/dashboard/project/add-template.tsx index b42806e52..06d0adf39 100644 --- a/apps/dokploy/components/dashboard/project/add-template.tsx +++ b/apps/dokploy/components/dashboard/project/add-template.tsx @@ -73,11 +73,11 @@ import { api } from "@/utils/api"; const TEMPLATE_BASE_URL_KEY = "dokploy_template_base_url"; interface Props { - projectId: string; + environmentId: string; baseUrl?: string; } -export const AddTemplate = ({ projectId, baseUrl }: Props) => { +export const AddTemplate = ({ environmentId, baseUrl }: Props) => { const [query, setQuery] = useState(""); const [open, setOpen] = useState(false); const [viewMode, setViewMode] = useState<"detailed" | "icon">("detailed"); @@ -91,6 +91,9 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => { 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) { @@ -490,7 +493,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => { disabled={isLoading} onClick={async () => { const promise = mutateAsync({ - projectId, + environmentId, serverId: serverId || undefined, id: template.id, baseUrl: customBaseUrl, @@ -498,8 +501,9 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => { toast.promise(promise, { loading: "Setting up...", success: () => { - utils.project.one.invalidate({ - projectId, + // Invalidate the project query to refresh the environment data + utils.environment.one.invalidate({ + environmentId, }); setOpen(false); return `${template.name} template created successfully`; diff --git a/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx b/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx new file mode 100644 index 000000000..b5cb9aa31 --- /dev/null +++ b/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx @@ -0,0 +1,382 @@ +import { useState } from "react"; +import { useRouter } from "next/router"; +import { api } from "@/utils/api"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { toast } from "sonner"; +import { ChevronDownIcon, PlusIcon, PencilIcon, TrashIcon } from "lucide-react"; + +interface Environment { + environmentId: string; + name: string; + description?: string | null; + createdAt: string; +} + +interface AdvancedEnvironmentSelectorProps { + projectId: string; + environments: Environment[]; + currentEnvironmentId?: string; +} + +export const AdvancedEnvironmentSelector = ({ + projectId, + environments, + currentEnvironmentId, +}: AdvancedEnvironmentSelectorProps) => { + const router = useRouter(); + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [selectedEnvironment, setSelectedEnvironment] = useState(null); + + // Form states + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + + // API mutations + const createEnvironment = api.environment.create.useMutation(); + const updateEnvironment = api.environment.update.useMutation(); + const deleteEnvironment = api.environment.remove.useMutation(); + const duplicateEnvironment = api.environment.duplicate.useMutation(); + + // Refetch project data + const utils = api.useUtils(); + + const handleCreateEnvironment = async () => { + try { + await createEnvironment.mutateAsync({ + projectId, + name: name.trim(), + description: description.trim() || null, + }); + + toast.success("Environment created successfully"); + utils.project.one.invalidate({ projectId }); + setIsCreateDialogOpen(false); + setName(""); + setDescription(""); + } catch (error) { + toast.error("Failed to create environment"); + } + }; + + const handleUpdateEnvironment = async () => { + if (!selectedEnvironment) return; + + try { + await updateEnvironment.mutateAsync({ + environmentId: selectedEnvironment.environmentId, + name: name.trim(), + description: description.trim() || null, + }); + + toast.success("Environment updated successfully"); + utils.project.one.invalidate({ projectId }); + setIsEditDialogOpen(false); + setSelectedEnvironment(null); + setName(""); + setDescription(""); + } catch (error) { + toast.error("Failed to update environment"); + } + }; + + const handleDeleteEnvironment = async () => { + if (!selectedEnvironment) return; + + try { + await deleteEnvironment.mutateAsync({ + environmentId: selectedEnvironment.environmentId, + }); + + toast.success("Environment deleted successfully"); + utils.project.one.invalidate({ projectId }); + setIsDeleteDialogOpen(false); + setSelectedEnvironment(null); + + // Redirect to production if we deleted the current environment + if (selectedEnvironment.environmentId === currentEnvironmentId) { + const productionEnv = environments.find(env => env.name === "production"); + if (productionEnv) { + router.push(`/dashboard/project/${projectId}/environment/${productionEnv.environmentId}`); + } + } + } catch (error) { + toast.error("Failed to delete environment"); + } + }; + + const handleDuplicateEnvironment = async (environment: Environment) => { + try { + const result = await duplicateEnvironment.mutateAsync({ + environmentId: environment.environmentId, + name: `${environment.name}-copy`, + description: environment.description, + }); + + toast.success("Environment duplicated successfully"); + utils.project.one.invalidate({ projectId }); + + // Navigate to the new duplicated environment + router.push(`/dashboard/project/${projectId}/environment/${result.environmentId}`); + } catch (error) { + toast.error("Failed to duplicate environment"); + } + }; + + const openEditDialog = (environment: Environment) => { + setSelectedEnvironment(environment); + setName(environment.name); + setDescription(environment.description || ""); + setIsEditDialogOpen(true); + }; + + const openDeleteDialog = (environment: Environment) => { + setSelectedEnvironment(environment); + setIsDeleteDialogOpen(true); + }; + + const currentEnv = environments.find(env => env.environmentId === currentEnvironmentId); + + return ( + <> + + + + + + Environments + + + {environments.map((environment) => ( +
+ { + router.push(`/dashboard/project/${projectId}/environment/${environment.environmentId}`); + }} + > +
+
+ {environment.name} + {environment.name === "production" && ( + + Prod + + )} +
+ {environment.environmentId === currentEnvironmentId && ( +
+ )} +
+ + + {/* Action buttons for non-production environments */} + {environment.name !== "production" && ( +
+ + +
+ )} +
+ ))} + + + setIsCreateDialogOpen(true)} + > + + Create Environment + + + + + {/* Create Environment Dialog */} + + + + Create Environment + + Create a new environment for your project. + + + +
+
+ + setName(e.target.value)} + placeholder="Environment name" + /> +
+
+ +