diff --git a/Dockerfile b/Dockerfile index 8da1db45..8b9d215c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,7 +27,7 @@ WORKDIR /app # Set production ENV NODE_ENV=production -RUN apt-get update && apt-get install -y curl apache2-utils && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var/lib/apt/lists/* # Copy only the necessary files COPY --from=build /prod/dokploy/.next ./.next @@ -42,7 +42,7 @@ COPY --from=build /prod/dokploy/node_modules ./node_modules # Install docker -RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh +RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh && curl https://rclone.org/install.sh | bash # Install Nixpacks and tsx # | VERBOSE=1 VERSION=1.21.0 bash @@ -55,4 +55,4 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \ COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack EXPOSE 3000 -CMD [ "pnpm", "start" ] +CMD [ "pnpm", "start" ] \ No newline at end of file diff --git a/apps/dokploy/LICENSE.MD b/apps/dokploy/LICENSE.MD index 9031c94b..59e9d822 100644 --- a/apps/dokploy/LICENSE.MD +++ b/apps/dokploy/LICENSE.MD @@ -17,10 +17,10 @@ See the License for the specific language governing permissions and limitations ## Additional Terms for Specific Features -The following additional terms apply to the multi-node support and Docker Compose file support features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License: +The following additional terms apply to the multi-node support, Docker Compose file and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License: -- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support and Docker Compose file support, will always be free to use in the self-hosted version. -- **Restriction on Resale**: The multi-node support and Docker Compose file support features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent. -- **Modification Distribution**: Any modifications to the multi-node support and Docker Compose file support features must be distributed freely and cannot be sold or offered as a service. +- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support and Multi Server, will always be free to use in the self-hosted version. +- **Restriction on Resale**: The multi-node support, Docker Compose file support and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent. +- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support and Multi Server features must be distributed freely and cannot be sold or offered as a service. For further inquiries or permissions, please contact us directly. diff --git a/apps/dokploy/__test__/drop/drop.test.test.ts b/apps/dokploy/__test__/drop/drop.test.test.ts index 5561999c..c411566a 100644 --- a/apps/dokploy/__test__/drop/drop.test.test.ts +++ b/apps/dokploy/__test__/drop/drop.test.test.ts @@ -1,6 +1,8 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { APPLICATIONS_PATH } from "@/server/constants"; +import { paths } from "@/server/constants"; +const { APPLICATIONS_PATH } = paths(); +import type { ApplicationNested } from "@/server/utils/builders"; import { unzipDrop } from "@/server/utils/builders/drop"; import AdmZip from "adm-zip"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; @@ -11,11 +13,84 @@ if (typeof window === "undefined") { globalThis.FileList = undici.FileList as any; } +const baseApp: ApplicationNested = { + applicationId: "", + applicationStatus: "done", + appName: "", + autoDeploy: true, + serverId: "", + branch: null, + dockerBuildStage: "", + buildArgs: null, + buildPath: "/", + gitlabPathNamespace: "", + buildType: "nixpacks", + bitbucketBranch: "", + bitbucketBuildPath: "", + bitbucketId: "", + bitbucketRepository: "", + bitbucketOwner: "", + githubId: "", + gitlabProjectId: 0, + gitlabBranch: "", + gitlabBuildPath: "", + gitlabId: "", + gitlabRepository: "", + gitlabOwner: "", + command: null, + cpuLimit: null, + cpuReservation: null, + createdAt: "", + customGitBranch: "", + customGitBuildPath: "", + customGitSSHKeyId: null, + customGitUrl: "", + description: "", + dockerfile: null, + dockerImage: null, + dropBuildPath: null, + enabled: null, + env: null, + healthCheckSwarm: null, + labelsSwarm: null, + memoryLimit: null, + memoryReservation: null, + modeSwarm: null, + mounts: [], + name: "", + networkSwarm: null, + owner: null, + password: null, + placementSwarm: null, + ports: [], + projectId: "", + publishDirectory: null, + redirects: [], + refreshToken: "", + registry: null, + registryId: null, + replicas: 1, + repository: null, + restartPolicySwarm: null, + rollbackConfigSwarm: null, + security: [], + sourceType: "git", + subtitle: null, + title: null, + updateConfigSwarm: null, + username: null, + dockerContextPath: null, +}; +// vi.mock("@/server/constants", () => ({ - APPLICATIONS_PATH: "./__test__/drop/zips/output", + paths: () => ({ + APPLICATIONS_PATH: "./__test__/drop/zips/output", + }), + // APPLICATIONS_PATH: "./__test__/drop/zips/output", })); describe("unzipDrop using real zip files", () => { + // const { APPLICATIONS_PATH } = paths(); beforeAll(async () => { await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true }); }); @@ -25,39 +100,42 @@ describe("unzipDrop using real zip files", () => { }); it("should correctly extract a zip with a single root folder", async () => { - const appName = "single-file"; - const outputPath = path.join(APPLICATIONS_PATH, appName, "code"); + baseApp.appName = "single-file"; + // const appName = "single-file"; + const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); const zip = new AdmZip("./__test__/drop/zips/single-file.zip"); const zipBuffer = zip.toBuffer(); const file = new File([zipBuffer], "single.zip"); - await unzipDrop(file, appName); + await unzipDrop(file, baseApp); const files = await fs.readdir(outputPath, { withFileTypes: true }); expect(files.some((f) => f.name === "test.txt")).toBe(true); }); it("should correctly extract a zip with a single root folder and a subfolder", async () => { - const appName = "folderwithfile"; - const outputPath = path.join(APPLICATIONS_PATH, appName, "code"); + baseApp.appName = "folderwithfile"; + // const appName = "folderwithfile"; + const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip"); const zipBuffer = zip.toBuffer(); const file = new File([zipBuffer], "single.zip"); - await unzipDrop(file, appName); + await unzipDrop(file, baseApp); const files = await fs.readdir(outputPath, { withFileTypes: true }); expect(files.some((f) => f.name === "folder1.txt")).toBe(true); }); it("should correctly extract a zip with multiple root folders", async () => { - const appName = "two-folders"; - const outputPath = path.join(APPLICATIONS_PATH, appName, "code"); + baseApp.appName = "two-folders"; + // const appName = "two-folders"; + const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); const zip = new AdmZip("./__test__/drop/zips/two-folders.zip"); const zipBuffer = zip.toBuffer(); const file = new File([zipBuffer], "single.zip"); - await unzipDrop(file, appName); + await unzipDrop(file, baseApp); const files = await fs.readdir(outputPath, { withFileTypes: true }); @@ -66,13 +144,14 @@ describe("unzipDrop using real zip files", () => { }); it("should correctly extract a zip with a single root with a file", async () => { - const appName = "nested"; - const outputPath = path.join(APPLICATIONS_PATH, appName, "code"); + baseApp.appName = "nested"; + // const appName = "nested"; + const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); const zip = new AdmZip("./__test__/drop/zips/nested.zip"); const zipBuffer = zip.toBuffer(); const file = new File([zipBuffer], "single.zip"); - await unzipDrop(file, appName); + await unzipDrop(file, baseApp); const files = await fs.readdir(outputPath, { withFileTypes: true }); @@ -82,13 +161,14 @@ describe("unzipDrop using real zip files", () => { }); it("should correctly extract a zip with a single root with a folder", async () => { - const appName = "folder-with-sibling-file"; - const outputPath = path.join(APPLICATIONS_PATH, appName, "code"); + baseApp.appName = "folder-with-sibling-file"; + // const appName = "folder-with-sibling-file"; + const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip"); const zipBuffer = zip.toBuffer(); const file = new File([zipBuffer], "single.zip"); - await unzipDrop(file, appName); + await unzipDrop(file, baseApp); const files = await fs.readdir(outputPath, { withFileTypes: true }); diff --git a/apps/dokploy/__test__/traefik/traefik.test.ts b/apps/dokploy/__test__/traefik/traefik.test.ts index 7ca9f169..222f8fd7 100644 --- a/apps/dokploy/__test__/traefik/traefik.test.ts +++ b/apps/dokploy/__test__/traefik/traefik.test.ts @@ -9,6 +9,7 @@ const baseApp: ApplicationNested = { applicationStatus: "done", appName: "", autoDeploy: true, + serverId: "", branch: null, dockerBuildStage: "", buildArgs: null, diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx index fd91703b..6750527d 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx @@ -278,6 +278,12 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { {isError && {error?.message}} +
+ + Changing settings such as placements may cause the logs/monitoring + to be unavailable. + +
{ - const { data } = api.application.readTraefikConfig.useQuery( + const { data, isLoading } = api.application.readTraefikConfig.useQuery( { applicationId, }, @@ -35,7 +35,12 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => { - {data === null ? ( + {isLoading ? ( + + Loading... + + + ) : !data ? (
diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx index 13a7703b..c24d8781 100644 --- a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx @@ -7,7 +7,7 @@ import { CardTitle, } from "@/components/ui/card"; import { api } from "@/utils/api"; -import { AlertTriangle, Package } from "lucide-react"; +import { Package } from "lucide-react"; import React from "react"; import { AddVolumes } from "./add-volumes"; import { DeleteVolume } from "./delete-volume"; diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx index 4af5c429..2e892523 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx @@ -11,8 +11,9 @@ interface Props { logPath: string | null; open: boolean; onClose: () => void; + serverId?: string; } -export const ShowDeployment = ({ logPath, open, onClose }: Props) => { +export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => { const [data, setData] = useState(""); const endOfLogsRef = useRef(null); @@ -21,7 +22,7 @@ export const ShowDeployment = ({ logPath, open, onClose }: Props) => { const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}`; + const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}${serverId ? `&serverId=${serverId}` : ""}`; const ws = new WebSocket(wsUrl); ws.onmessage = (e) => { diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx index 0458bbbb..c2288bb8 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx @@ -25,7 +25,7 @@ export const ShowDeployments = ({ applicationId }: Props) => { { applicationId }, { enabled: !!applicationId, - refetchInterval: 5000, + refetchInterval: 1000, }, ); const [url, setUrl] = React.useState(""); @@ -110,6 +110,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
)} setActiveLog(null)} logPath={activeLog} diff --git a/apps/dokploy/components/dashboard/application/domains/add-domain.tsx b/apps/dokploy/components/dashboard/application/domains/add-domain.tsx index d77796d0..43a3cb69 100644 --- a/apps/dokploy/components/dashboard/application/domains/add-domain.tsx +++ b/apps/dokploy/components/dashboard/application/domains/add-domain.tsx @@ -175,6 +175,7 @@ export const AddDomain = ({ onClick={() => { generateDomain({ appName: application?.appName || "", + serverId: application?.serverId || "", }) .then((domain) => { field.onChange(domain); @@ -296,11 +297,7 @@ export const AddDomain = ({ - diff --git a/apps/dokploy/components/dashboard/application/general/deploy-application.tsx b/apps/dokploy/components/dashboard/application/general/deploy-application.tsx index d8a33384..f9115c76 100644 --- a/apps/dokploy/components/dashboard/application/general/deploy-application.tsx +++ b/apps/dokploy/components/dashboard/application/general/deploy-application.tsx @@ -45,14 +45,17 @@ export const DeployApplication = ({ applicationId }: Props) => { Cancel { - toast.success("Deploying Application...."); - - await refetch(); await deploy({ applicationId, - }).catch(() => { - toast.error("Error to deploy Application"); - }); + }) + .then(async () => { + toast.success("Application deployed succesfully"); + await refetch(); + }) + + .catch(() => { + toast.error("Error to deploy Application"); + }); await refetch(); }} diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx index 28c34963..4ed9df16 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx @@ -130,7 +130,7 @@ export const SaveDragNDrop = ({ applicationId }: Props) => { type="submit" className="w-fit" isLoading={isLoading} - disabled={!zip} + disabled={!zip || isLoading} > Deploy{" "} diff --git a/apps/dokploy/components/dashboard/application/general/show.tsx b/apps/dokploy/components/dashboard/application/general/show.tsx index 870f5d54..277ae1eb 100644 --- a/apps/dokploy/components/dashboard/application/general/show.tsx +++ b/apps/dokploy/components/dashboard/application/general/show.tsx @@ -66,7 +66,10 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => { ) : ( )} - + - + {data && data?.length > 0 && ( +
+
+ + +
- + )} ); diff --git a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx index d8f87f39..876d6838 100644 --- a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx +++ b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx @@ -18,10 +18,15 @@ const Terminal = dynamic( interface Props { containerId: string; + serverId?: string; children?: React.ReactNode; } -export const DockerTerminalModal = ({ children, containerId }: Props) => { +export const DockerTerminalModal = ({ + children, + containerId, + serverId, +}: Props) => { return ( @@ -40,7 +45,11 @@ export const DockerTerminalModal = ({ children, containerId }: Props) => { - + ); diff --git a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx index 03001af7..4008d6fd 100644 --- a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx +++ b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx @@ -8,9 +8,14 @@ import { AttachAddon } from "@xterm/addon-attach"; interface Props { id: string; containerId: string; + serverId?: string; } -export const DockerTerminal: React.FC = ({ id, containerId }) => { +export const DockerTerminal: React.FC = ({ + id, + containerId, + serverId, +}) => { const termRef = useRef(null); const [activeWay, setActiveWay] = React.useState("bash"); useEffect(() => { @@ -33,7 +38,7 @@ export const DockerTerminal: React.FC = ({ id, containerId }) => { const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}`; + const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}${serverId ? `&serverId=${serverId}` : ""}`; const ws = new WebSocket(wsUrl); diff --git a/apps/dokploy/components/dashboard/file-system/show-traefik-file.tsx b/apps/dokploy/components/dashboard/file-system/show-traefik-file.tsx index d2dc1d78..3dfe9875 100644 --- a/apps/dokploy/components/dashboard/file-system/show-traefik-file.tsx +++ b/apps/dokploy/components/dashboard/file-system/show-traefik-file.tsx @@ -13,6 +13,7 @@ import { } from "@/components/ui/form"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; +import { Loader2 } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -29,12 +30,18 @@ type UpdateServerMiddlewareConfig = z.infer< interface Props { path: string; + serverId?: string; } -export const ShowTraefikFile = ({ path }: Props) => { - const { data, refetch } = api.settings.readTraefikFile.useQuery( +export const ShowTraefikFile = ({ path, serverId }: Props) => { + const { + data, + refetch, + isLoading: isLoadingFile, + } = api.settings.readTraefikFile.useQuery( { path, + serverId, }, { enabled: !!path, @@ -54,11 +61,9 @@ export const ShowTraefikFile = ({ path }: Props) => { }); useEffect(() => { - if (data) { - form.reset({ - traefikConfig: data || "", - }); - } + form.reset({ + traefikConfig: data || "", + }); }, [form, form.reset, data]); const onSubmit = async (data: UpdateServerMiddlewareConfig) => { @@ -74,6 +79,7 @@ export const ShowTraefikFile = ({ path }: Props) => { await mutateAsync({ traefikConfig: data.traefikConfig, path, + serverId, }) .then(async () => { toast.success("Traefik config Updated"); @@ -93,20 +99,28 @@ export const ShowTraefikFile = ({ path }: Props) => { className="w-full relative z-[5]" >
- ( - - Traefik config - - {path} - - - + + Loading... + + +
+ ) : ( + ( + + Traefik config + + {path} + + + - + {...field} + /> + -
-										
-									
-
- -
-
- )} - /> +
+											
+										
+
+ +
+ + )} + /> + )}
-
diff --git a/apps/dokploy/components/dashboard/file-system/show-traefik-system.tsx b/apps/dokploy/components/dashboard/file-system/show-traefik-system.tsx index e3e874c5..0aaf9990 100644 --- a/apps/dokploy/components/dashboard/file-system/show-traefik-system.tsx +++ b/apps/dokploy/components/dashboard/file-system/show-traefik-system.tsx @@ -1,20 +1,47 @@ -import React from "react"; - +import { AlertBlock } from "@/components/shared/alert-block"; import { Tree } from "@/components/ui/file-tree"; -import { api } from "@/utils/api"; -import { FileIcon, Folder, Workflow } from "lucide-react"; - import { cn } from "@/lib/utils"; +import { api } from "@/utils/api"; +import { FileIcon, Folder, Loader2, Workflow } from "lucide-react"; +import React from "react"; import { ShowTraefikFile } from "./show-traefik-file"; -export const ShowTraefikSystem = () => { +interface Props { + serverId?: string; +} +export const ShowTraefikSystem = ({ serverId }: Props) => { const [file, setFile] = React.useState(null); - const { data: directories } = api.settings.readDirectories.useQuery(); + const { + data: directories, + isLoading, + error, + isError, + } = api.settings.readDirectories.useQuery( + { + serverId, + }, + { + retry: 2, + }, + ); return (
+ {isError && ( + + {error?.message} + + )} + {isLoading && ( +
+ + Loading... + + +
+ )} {directories?.length === 0 && (
@@ -34,7 +61,7 @@ export const ShowTraefikSystem = () => { />
{file ? ( - + ) : (
diff --git a/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx b/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx index 44b6e39c..925e213d 100644 --- a/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx +++ b/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx @@ -36,7 +36,10 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => { ) : ( )} - +
+ ( + + + + + + Select a Server (Optional) + + + + + + If not server is selected, the application will be + deployed on the server where the user is logged in. + + + + + + + + + )} + /> { const utils = api.useUtils(); const [visible, setVisible] = useState(false); const slug = slugify(projectName); + const { data: servers } = api.server.withSSHKey.useQuery(); const postgresMutation = api.postgres.create.useMutation(); const mongoMutation = api.mongo.create.useMutation(); const redisMutation = api.redis.create.useMutation(); @@ -161,6 +172,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { description: "", databaseName: "", databaseUser: "", + serverId: null, }, resolver: zodResolver(mySchema), }); @@ -183,6 +195,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { appName: data.appName, dockerImage: defaultDockerImage, projectId, + serverId: data.serverId, description: data.description, }; @@ -191,8 +204,10 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { ...commonParams, databasePassword: data.databasePassword, databaseName: data.databaseName, + databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], + serverId: data.serverId, }); } else if (data.type === "mongo") { promise = mongoMutation.mutateAsync({ @@ -200,11 +215,13 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { databasePassword: data.databasePassword, databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], + serverId: data.serverId, }); } else if (data.type === "redis") { promise = redisMutation.mutateAsync({ ...commonParams, databasePassword: data.databasePassword, + serverId: data.serverId, projectId, }); } else if (data.type === "mariadb") { @@ -215,6 +232,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { databaseName: data.databaseName, databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], + serverId: data.serverId, }); } else if (data.type === "mysql") { promise = mysqlMutation.mutateAsync({ @@ -224,6 +242,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], databaseRootPassword: data.databaseRootPassword, + serverId: data.serverId, }); } @@ -352,6 +371,39 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { )} /> + ( + + Select a Server + + + + )} + /> { const [open, setOpen] = useState(false); const { data } = api.compose.templates.useQuery(); const [selectedTags, setSelectedTags] = useState([]); + const { data: servers } = api.server.withSSHKey.useQuery(); const { data: tags, isLoading: isLoadingTags } = api.compose.getTags.useQuery(); const utils = api.useUtils(); + + const [serverId, setServerId] = useState(undefined); const { mutateAsync, isLoading, error, isError } = api.compose.deployTemplate.useMutation(); @@ -109,7 +129,6 @@ export const AddTemplate = ({ projectId }: Props) => { role="combobox" className={cn( "md:max-w-[15rem] w-full justify-between !bg-input", - // !field.value && "text-muted-foreground", )} > {isLoadingTags @@ -268,30 +287,79 @@ export const AddTemplate = ({ projectId }: Props) => { {template.name} template and add it to your project. + +
+ + + + + + + + If not server is selected, the + application will be deployed on the + server where the user is logged in. + + + + + + +
Cancel { - await mutateAsync({ + const promise = mutateAsync({ projectId, + serverId: serverId || undefined, id: template.id, - }) - .then(async () => { - toast.success( - `Succesfully created ${template.name} application from template`, - ); - + }); + toast.promise(promise, { + loading: "Setting up...", + success: (data) => { utils.project.one.invalidate({ projectId, }); setOpen(false); - }) - .catch(() => { - toast.error( - `Error creating ${template.name} application from template`, - ); - }); + return `${template.name} template created succesfully`; + }, + error: (err) => { + return `Ocurred an error deploying ${template.name} template`; + }, + }); }} > Confirm diff --git a/apps/dokploy/components/dashboard/projects/add.tsx b/apps/dokploy/components/dashboard/projects/add.tsx index 1b9f37f8..bd8f268f 100644 --- a/apps/dokploy/components/dashboard/projects/add.tsx +++ b/apps/dokploy/components/dashboard/projects/add.tsx @@ -17,6 +17,7 @@ import { FormLabel, FormMessage, } from "@/components/ui/form"; + import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/utils/api"; @@ -109,6 +110,7 @@ export const AddProject = () => { )} />
+ { )} - + + + + Actions + + + { + await reloadServer() + .then(async () => { + toast.success("Server Reloaded"); + }) + .catch(() => { + toast.success("Server Reloaded"); + }); + }} + > + Reload + + + Watch logs + + + + + ); +}; diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/show-server-actions.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/show-server-actions.tsx new file mode 100644 index 00000000..1f7f2d14 --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/actions/show-server-actions.tsx @@ -0,0 +1,44 @@ +import { CardDescription, CardTitle } from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; +import { useState } from "react"; +import { ShowStorageActions } from "./show-storage-actions"; +import { ShowTraefikActions } from "./show-traefik-actions"; +import { ToggleDockerCleanup } from "./toggle-docker-cleanup"; +interface Props { + serverId: string; +} + +export const ShowServerActions = ({ serverId }: Props) => { + const [isOpen, setIsOpen] = useState(false); + return ( + + + e.preventDefault()} + > + View Actions + + + +
+ Web server settings + Reload or clean the web server. +
+ +
+ + + +
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/show-storage-actions.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/show-storage-actions.tsx new file mode 100644 index 00000000..b3f9c334 --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/actions/show-storage-actions.tsx @@ -0,0 +1,177 @@ +import { Button } from "@/components/ui/button"; +import React from "react"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { api } from "@/utils/api"; +import { toast } from "sonner"; + +interface Props { + serverId?: string; +} +export const ShowStorageActions = ({ serverId }: Props) => { + const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } = + api.settings.cleanAll.useMutation(); + + const { + mutateAsync: cleanDockerBuilder, + isLoading: cleanDockerBuilderIsLoading, + } = api.settings.cleanDockerBuilder.useMutation(); + + const { mutateAsync: cleanMonitoring, isLoading: cleanMonitoringIsLoading } = + api.settings.cleanMonitoring.useMutation(); + const { + mutateAsync: cleanUnusedImages, + isLoading: cleanUnusedImagesIsLoading, + } = api.settings.cleanUnusedImages.useMutation(); + + const { + mutateAsync: cleanUnusedVolumes, + isLoading: cleanUnusedVolumesIsLoading, + } = api.settings.cleanUnusedVolumes.useMutation(); + + const { + mutateAsync: cleanStoppedContainers, + isLoading: cleanStoppedContainersIsLoading, + } = api.settings.cleanStoppedContainers.useMutation(); + + return ( + + + + + + Actions + + + { + await cleanUnusedImages({ + serverId: serverId, + }) + .then(async () => { + toast.success("Cleaned images"); + }) + .catch(() => { + toast.error("Error to clean images"); + }); + }} + > + Clean unused images + + { + await cleanUnusedVolumes({ + serverId: serverId, + }) + .then(async () => { + toast.success("Cleaned volumes"); + }) + .catch(() => { + toast.error("Error to clean volumes"); + }); + }} + > + Clean unused volumes + + + { + await cleanStoppedContainers({ + serverId: serverId, + }) + .then(async () => { + toast.success("Stopped containers cleaned"); + }) + .catch(() => { + toast.error("Error to clean stopped containers"); + }); + }} + > + Clean stopped containers + + + { + await cleanDockerBuilder({ + serverId: serverId, + }) + .then(async () => { + toast.success("Cleaned Docker Builder"); + }) + .catch(() => { + toast.error("Error to clean Docker Builder"); + }); + }} + > + Clean Docker Builder & System + + {!serverId && ( + { + await cleanMonitoring() + .then(async () => { + toast.success("Cleaned Monitoring"); + }) + .catch(() => { + toast.error("Error to clean Monitoring"); + }); + }} + > + Clean Monitoring + + )} + + { + await cleanAll({ + serverId: serverId, + }) + .then(async () => { + toast.success("Cleaned all"); + }) + .catch(() => { + toast.error("Error to clean all"); + }); + }} + > + Clean all + + + + + ); +}; diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx new file mode 100644 index 00000000..a0ea3f5e --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx @@ -0,0 +1,125 @@ +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import React from "react"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { api } from "@/utils/api"; +import { toast } from "sonner"; + +import { cn } from "@/lib/utils"; +import { EditTraefikEnv } from "../../web-server/edit-traefik-env"; +import { ShowModalLogs } from "../../web-server/show-modal-logs"; + +interface Props { + serverId?: string; +} +export const ShowTraefikActions = ({ serverId }: Props) => { + const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } = + api.settings.reloadTraefik.useMutation(); + + const { mutateAsync: toggleDashboard, isLoading: toggleDashboardIsLoading } = + api.settings.toggleDashboard.useMutation(); + + const { data: haveTraefikDashboardPortEnabled, refetch: refetchDashboard } = + api.settings.haveTraefikDashboardPortEnabled.useQuery({ + serverId, + }); + + return ( + + + + + + Actions + + + { + await reloadTraefik({ + serverId: serverId, + }) + .then(async () => { + toast.success("Traefik Reloaded"); + }) + .catch(() => { + toast.error("Error to reload the traefik"); + }); + }} + > + Reload + + + Watch logs + + + e.preventDefault()} + className="w-full cursor-pointer space-x-3" + > + Modify Env + + + + { + await toggleDashboard({ + enableDashboard: !haveTraefikDashboardPortEnabled, + serverId: serverId, + }) + .then(async () => { + toast.success( + `${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`, + ); + refetchDashboard(); + }) + .catch(() => { + toast.error( + `${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`, + ); + }); + }} + className="w-full cursor-pointer space-x-3" + > + + {haveTraefikDashboardPortEnabled ? "Disable" : "Enable"} Dashboard + + + {/* + + e.preventDefault()} + > + Enter the terminal + + */} + + + + ); +}; diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/toggle-docker-cleanup.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/toggle-docker-cleanup.tsx new file mode 100644 index 00000000..17edaa99 --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/actions/toggle-docker-cleanup.tsx @@ -0,0 +1,52 @@ +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { api } from "@/utils/api"; +import { toast } from "sonner"; + +interface Props { + serverId?: string; +} +export const ToggleDockerCleanup = ({ serverId }: Props) => { + const { data, refetch } = api.admin.one.useQuery(undefined, { + enabled: !serverId, + }); + + const { data: server, refetch: refetchServer } = api.server.one.useQuery( + { + serverId: serverId || "", + }, + { + enabled: !!serverId, + }, + ); + + const enabled = data?.enableDockerCleanup || server?.enableDockerCleanup; + + const { mutateAsync } = api.settings.updateDockerCleanup.useMutation(); + return ( +
+ { + await mutateAsync({ + enableDockerCleanup: e, + serverId: serverId, + }) + .then(async () => { + toast.success("Docker Cleanup Enabled"); + }) + .catch(() => { + toast.error("Docker Cleanup Error"); + }); + + if (serverId) { + refetchServer(); + } else { + refetch(); + } + }} + /> + +
+ ); +}; diff --git a/apps/dokploy/components/dashboard/settings/servers/add-server.tsx b/apps/dokploy/components/dashboard/settings/servers/add-server.tsx new file mode 100644 index 00000000..6bd44dcf --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/add-server.tsx @@ -0,0 +1,253 @@ +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 { + 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 { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { PlusIcon } from "lucide-react"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +const Schema = z.object({ + name: z.string().min(1, { + message: "Name is required", + }), + description: z.string().optional(), + ipAddress: z.string().min(1, { + message: "IP Address is required", + }), + port: z.number().optional(), + username: z.string().optional(), + sshKeyId: z.string().min(1, { + message: "SSH Key is required", + }), +}); + +type Schema = z.infer; + +export const AddServer = () => { + const utils = api.useUtils(); + const [isOpen, setIsOpen] = useState(false); + const { data: sshKeys } = api.sshKey.all.useQuery(); + const { mutateAsync, error, isError } = api.server.create.useMutation(); + const form = useForm({ + defaultValues: { + description: "", + name: "", + ipAddress: "", + port: 22, + username: "root", + sshKeyId: "", + }, + resolver: zodResolver(Schema), + }); + + useEffect(() => { + form.reset({ + description: "", + name: "", + ipAddress: "", + port: 22, + username: "root", + sshKeyId: "", + }); + }, [form, form.reset, form.formState.isSubmitSuccessful]); + + const onSubmit = async (data: Schema) => { + await mutateAsync({ + name: data.name, + description: data.description || "", + ipAddress: data.ipAddress || "", + port: data.port || 22, + username: data.username || "root", + sshKeyId: data.sshKeyId || "", + }) + .then(async (data) => { + await utils.server.all.invalidate(); + toast.success("Server Created"); + setIsOpen(false); + }) + .catch(() => { + toast.error("Error to create a server"); + }); + }; + + return ( + + + + + + + Add Server + + Add a server to deploy your applications remotely. + + + {isError && {error?.message}} +
+ +
+ ( + + Name + + + + + + + )} + /> +
+ ( + + Description + +