From 72f8a28f4f72bc42759c9246466b9902e946f5a3 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Mon, 1 Sep 2025 19:48:20 -0600 Subject: [PATCH] refactor: update project structure to use environmentId instead of projectId across components and API routes; implement environment management features --- .../dashboard/project/add-ai-assistant.tsx | 6 +- .../dashboard/project/add-application.tsx | 10 +- .../dashboard/project/add-compose.tsx | 14 +- .../dashboard/project/add-database.tsx | 16 +- .../dashboard/project/add-template.tsx | 14 +- .../project/advanced-environment-selector.tsx | 382 + .../project/ai/template-generator.tsx | 14 +- .../components/dashboard/projects/show.tsx | 64 +- .../components/dashboard/search-command.tsx | 12 +- apps/dokploy/drizzle/0109_clammy_kabuki.sql | 23 + apps/dokploy/drizzle/meta/0109_snapshot.json | 6481 +++++++++++++++++ apps/dokploy/drizzle/meta/_journal.json | 7 + .../pages/dashboard/project/[projectId].tsx | 1278 +--- .../environment/[environmentId].tsx | 1417 ++++ .../dokploy/server/api/routers/application.ts | 11 +- apps/dokploy/server/api/routers/compose.ts | 20 +- apps/dokploy/server/api/routers/mariadb.ts | 14 +- apps/dokploy/server/api/routers/mongo.ts | 12 +- apps/dokploy/server/api/routers/mysql.ts | 14 +- apps/dokploy/server/api/routers/postgres.ts | 12 +- apps/dokploy/server/api/routers/redis.ts | 14 +- packages/server/src/db/schema/application.ts | 10 +- packages/server/src/db/schema/compose.ts | 11 +- packages/server/src/db/schema/mariadb.ts | 11 +- packages/server/src/db/schema/mongo.ts | 11 +- packages/server/src/db/schema/mysql.ts | 11 +- packages/server/src/db/schema/postgres.ts | 11 +- packages/server/src/db/schema/redis.ts | 11 +- packages/server/src/services/application.ts | 5 +- packages/server/src/services/environment.ts | 10 + packages/server/src/services/project.ts | 2 +- 31 files changed, 8527 insertions(+), 1401 deletions(-) create mode 100644 apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx create mode 100644 apps/dokploy/drizzle/0109_clammy_kabuki.sql create mode 100644 apps/dokploy/drizzle/meta/0109_snapshot.json create mode 100644 apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx 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" + /> +
+
+ +