diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..0b849afc01 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,18 @@ +## What is this PR about? + +Please describe in a short paragraph what this PR is about. + +## Checklist + +Before submitting this PR, please make sure that: + +- [] You created a dedicated branch based on the `canary` branch. +- [] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request +- [] You have tested this PR in your local instance. + +## Issues related (if applicable) + +closes #123 + +## Screenshots (if applicable) + diff --git a/.github/sponsors/tuple.png b/.github/sponsors/tuple.png new file mode 100644 index 0000000000..1d7be47ca9 Binary files /dev/null and b/.github/sponsors/tuple.png differ diff --git a/.github/workflows/create-pr.yml b/.github/workflows/create-pr.yml index e3f6aa234c..248b98d5a7 100644 --- a/.github/workflows/create-pr.yml +++ b/.github/workflows/create-pr.yml @@ -19,17 +19,14 @@ jobs: fetch-depth: 0 - name: Get version from package.json - id: package_version run: echo "VERSION=$(jq -r .version ./apps/dokploy/package.json)" >> $GITHUB_ENV - name: Get latest GitHub tag - id: latest_tag run: | LATEST_TAG=$(git ls-remote --tags origin | awk -F'/' '{print $3}' | sort -V | tail -n1) echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV echo $LATEST_TAG - name: Compare versions - id: compare_versions run: | if [ "${{ env.VERSION }}" != "${{ env.LATEST_TAG }}" ]; then VERSION_CHANGED="true" @@ -42,7 +39,6 @@ jobs: echo "Latest tag: ${{ env.LATEST_TAG }}" echo "Version changed: $VERSION_CHANGED" - name: Check if a PR already exists - id: check_pr run: | PR_EXISTS=$(gh pr list --state open --base main --head canary --json number --jq '. | length') echo "PR_EXISTS=$PR_EXISTS" >> $GITHUB_ENV diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bb7721468c..3ed957b723 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,8 @@ name: Build Docker images on: push: - branches: ["canary", "main", "feat/monitoring"] + branches: [main, canary] + workflow_dispatch: jobs: build-and-push-cloud-image: diff --git a/.github/workflows/dokploy.yml b/.github/workflows/dokploy.yml index 0f65a50c99..529cd8f7fa 100644 --- a/.github/workflows/dokploy.yml +++ b/.github/workflows/dokploy.yml @@ -2,7 +2,8 @@ name: Dokploy Docker Build on: push: - branches: [main, canary, "1061-custom-docker-service-hostname"] + branches: [main, canary, "fix/re-apply-database-migration-fix"] + workflow_dispatch: env: IMAGE_NAME: dokploy/dokploy diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 827ccc709c..cfddad7b2f 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -11,12 +11,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup biomeJs uses: biomejs/setup-biome@v2 - name: Run Biome formatter - run: biome format . --write + run: biome format --write - - uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef + - uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 # v1.3.2 diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index e9591f3cc4..6c74dbc028 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -4,36 +4,15 @@ on: pull_request: branches: [main, canary] -jobs: - lint-and-typecheck: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20.16.0 - cache: "pnpm" - - run: pnpm install --frozen-lockfile - - run: pnpm run server:build - - run: pnpm typecheck +permissions: + contents: read - build-and-test: - needs: lint-and-typecheck - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20.16.0 - cache: "pnpm" - - run: pnpm install --frozen-lockfile - - run: pnpm run server:build - - run: pnpm build - - parallel-tests: +jobs: + pr-check: runs-on: ubuntu-latest + strategy: + matrix: + job: [build, test, typecheck] steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 @@ -42,5 +21,5 @@ jobs: node-version: 20.16.0 cache: "pnpm" - run: pnpm install --frozen-lockfile - - run: pnpm run server:build - - run: pnpm test + - run: pnpm server:build + - run: pnpm ${{ matrix.job }} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000..16e8e6664e --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["biomejs.biome"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..99357f2369 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "biomejs.biome", + "editor.codeActionsOnSave": { + "source.fixAll.biome": "explicit", + "source.organizeImports.biome": "explicit" + } +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0ac5a3581b..38a36345e6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -87,7 +87,8 @@ pnpm run dokploy:dev Go to http://localhost:3000 to see the development server -Note: this project uses Biome. If your editor is configured to use another formatter such as Prettier, it's recommended to either change it to use Biome or turn it off. +> [!NOTE] +> This project uses Biome. If your editor is configured to use another formatter such as Prettier, it's recommended to either change it to use Biome or turn it off. ## Build @@ -117,10 +118,10 @@ In the case you lost your password, you can reset it using the following command pnpm run reset-password ``` -If you want to test the webhooks on development mode using localtunnel, make sure to install `localtunnel` +If you want to test the webhooks on development mode using localtunnel, make sure to install [`localtunnel`](https://localtunnel.app/) ```bash -bunx lt --port 3000 +pnpm dlx localtunnel --port 3000 ``` If you run into permission issues of docker run the following command @@ -152,7 +153,7 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0. ## Pull Request -- The `main` branch is the source of truth and should always reflect the latest stable release. +- The `canary` branch is the source of truth and should always reflect the latest stable release. - Create a new branch for each feature or bug fix. - Make sure to add tests for your changes. - Make sure to update the documentation for any changes Go to the [docs.dokploy.com](https://docs.dokploy.com) website to see the changes. @@ -161,6 +162,12 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0. - If your pull request fixes an open issue, please reference the issue in the pull request description. - Once your pull request is merged, you will be automatically added as a contributor to the project. +**Important Considerations for Pull Requests:** + +- **Focus and Scope:** Each Pull Request should ideally address a single, well-defined problem or introduce one new feature. This greatly facilitates review and reduces the chances of introducing unintended side effects. +- **Avoid Unfocused Changes:** Please avoid submitting Pull Requests that contain only minor changes such as whitespace adjustments, IDE-generated formatting, or removal of unused variables, unless these are part of a larger, clearly defined refactor or a dedicated "cleanup" Pull Request that addresses a specific `good first issue` or maintenance task. +- **Issue Association:** For any significant change, it's highly recommended to open an issue first to discuss the proposed solution with the community and maintainers. This ensures alignment and avoids duplicated effort. If your PR resolves an existing issue, please link it in the description (e.g., `Fixes #123`, `Closes #456`). + Thank you for your contribution! ## Templates diff --git a/Dockerfile b/Dockerfile index c41df8c730..11310b18e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,9 @@ # syntax=docker/dockerfile:1 -FROM node:20.9-slim AS base +FROM node:20.16.0-slim AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable +RUN corepack prepare pnpm@9.12.0 --activate FROM base AS build COPY . /usr/src/app @@ -57,7 +58,7 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \ && pnpm install -g tsx # Install Railpack -ARG RAILPACK_VERSION=0.0.64 +ARG RAILPACK_VERSION=0.2.2 RUN curl -sSL https://railpack.com/install.sh | bash # Install buildpacks diff --git a/Dockerfile.cloud b/Dockerfile.cloud index c234259dc4..8e4bac2159 100644 --- a/Dockerfile.cloud +++ b/Dockerfile.cloud @@ -1,8 +1,9 @@ # syntax=docker/dockerfile:1 -FROM node:20.9-slim AS base +FROM node:20.16.0-slim AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable +RUN corepack prepare pnpm@9.12.0 --activate FROM base AS build COPY . /usr/src/app diff --git a/Dockerfile.schedule b/Dockerfile.schedule index 70976523c5..ecb125e091 100644 --- a/Dockerfile.schedule +++ b/Dockerfile.schedule @@ -1,8 +1,9 @@ # syntax=docker/dockerfile:1 -FROM node:20.9-slim AS base +FROM node:20.16.0-slim AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable +RUN corepack prepare pnpm@9.12.0 --activate FROM base AS build COPY . /usr/src/app diff --git a/Dockerfile.server b/Dockerfile.server index e911c87805..ea6b372e84 100644 --- a/Dockerfile.server +++ b/Dockerfile.server @@ -1,8 +1,9 @@ # syntax=docker/dockerfile:1 -FROM node:20.9-slim AS base +FROM node:20.16.0-slim AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable +RUN corepack prepare pnpm@9.12.0 --activate FROM base AS build COPY . /usr/src/app diff --git a/GUIDES.md b/GUIDES.md index cfb7cd8128..90fba522dc 100644 --- a/GUIDES.md +++ b/GUIDES.md @@ -16,28 +16,29 @@ Here's how to install docker on different operating systems: ### Ubuntu ```bash +# Uninstall old versions +for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done + # Update package index sudo apt-get update # Install prerequisites -sudo apt-get install \ - apt-transport-https \ - ca-certificates \ - curl \ - gnupg \ - lsb-release +sudo apt-get install ca-certificates curl +sudo install -m 0755 -d /etc/apt/keyrings # Add Docker's official GPG key -curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg +sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc +sudo chmod a+r /etc/apt/keyrings/docker.asc -# Set up stable repository +# Add the repository to Apt sources echo \ - "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \ - $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ + $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # Install Docker Engine sudo apt-get update -sudo apt-get install docker-ce docker-ce-cli containerd.io +sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin ``` ## Windows diff --git a/LICENSE.MD b/LICENSE.MD index 7e49a35ba8..6cbef2c6d1 100644 --- a/LICENSE.MD +++ b/LICENSE.MD @@ -2,7 +2,7 @@ ## Core License (Apache License 2.0) -Copyright 2024 Mauricio Siu. +Copyright 2025 Mauricio Siu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index d192d6f751..8faf22a356 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,36 @@
-
- - Dokploy - Open Source Alternative to Vercel, Heroku and Netlify. - -
- -
-
-
Join us on Discord for help, feedback, and discussions!
+ + Dokploy - Open Source Alternative to Vercel, Heroku and Netlify. + +

+

Join us on Discord for help, feedback, and discussions!

Discord Shield
-

+ + + +
+ Special thanks to: +
+
+ + Tuple's sponsorship image + + +### [Tuple, the premier screen sharing app for developers](https://tuple.app/dokploy) +[Available for MacOS & Windows](https://tuple.app/dokploy)
+ +
+ + Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases. -### Features + +## โœจ Features Dokploy includes multiple features to make your life easier. @@ -47,7 +60,7 @@ curl -sSL https://dokploy.com/install.sh | sh For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com). -## Sponsors +## โ™ฅ๏ธ Sponsors ๐Ÿ™ We're deeply grateful to all our sponsors who make Dokploy possible! Your support helps cover the costs of hosting, testing, and developing new features. @@ -61,76 +74,47 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com). ### Hero Sponsors ๐ŸŽ– -
- - Hostinger - - - LX Aer - - - Mandarin - - - Lightnode - - - +
+ Hostinger + LX Aer
-### Premium Supporters ๐Ÿฅ‡ + -
- - Supafort.com - + - - agentdock.ai - +### Premium Supporters ๐Ÿฅ‡ +
+ Supafort.com + agentdock.ai
-### Elite Contributors ๐Ÿฅˆ - -
- - - AmericanCloud - - -
- - -### Supporting Members ๐Ÿฅ‰ - -
-Lightspeed.run -Cloudblast.io -Startupfame -Itsdb-center -Openalternative -Synexa +### Elite Contributors ๐Ÿฅˆ +
+ AmericanCloud + Tolgee
+### Supporting Members ๐Ÿฅ‰ -### Community Backers ๐Ÿค +
-
-Steamsets.com -Rivo.gg -Rivo.gg + Cloudblast.io + Synexa
+### Community Backers ๐Ÿค + #### Organizations: -[![Sponsors on Open Collective](https://opencollective.com/dokploy/organizations.svg?width=890)](https://opencollective.com/dokploy) +[Sponsors on Open Collective](https://opencollective.com/dokploy) #### Individuals: @@ -139,15 +123,15 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com). ### Contributors ๐Ÿค - - + Contributors + -## Video Tutorial +## ๐Ÿ“บ Video Tutorial - Watch the video + Watch the video -## Contributing +## ๐Ÿค Contributing Check out the [Contributing Guide](CONTRIBUTING.md) for more information. diff --git a/apps/api/package.json b/apps/api/package.json index 65f9d4ad95..dfc2a355d1 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -9,25 +9,30 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "inngest": "3.40.1", "@dokploy/server": "workspace:*", - "@hono/node-server": "^1.12.1", + "@hono/node-server": "^1.14.3", "@hono/zod-validator": "0.3.0", "@nerimity/mimiqueue": "1.2.3", - "dotenv": "^16.3.1", - "hono": "^4.5.8", + "dotenv": "^16.4.5", + "hono": "^4.7.10", "pino": "9.4.0", "pino-pretty": "11.2.2", "react": "18.2.0", "react-dom": "18.2.0", "redis": "4.7.0", - "zod": "^3.23.4" + "zod": "^3.25.32" }, "devDependencies": { - "@types/node": "^20.11.17", + "@types/node": "^20.17.51", "@types/react": "^18.2.37", "@types/react-dom": "^18.2.15", - "tsx": "^4.7.1", - "typescript": "^5.4.2" + "tsx": "^4.16.2", + "typescript": "^5.8.3" }, - "packageManager": "pnpm@9.5.0" + "packageManager": "pnpm@9.12.0", + "engines": { + "node": "^20.16.0", + "pnpm": ">=9.12.0" + } } diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 0db5659950..8ddb56dec0 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -2,21 +2,90 @@ import { serve } from "@hono/node-server"; import { Hono } from "hono"; import "dotenv/config"; import { zValidator } from "@hono/zod-validator"; -import { Queue } from "@nerimity/mimiqueue"; -import { createClient } from "redis"; +import { Inngest } from "inngest"; +import { serve as serveInngest } from "inngest/hono"; import { logger } from "./logger.js"; -import { type DeployJob, deployJobSchema } from "./schema.js"; +import { + cancelDeploymentSchema, + type DeployJob, + deployJobSchema, +} from "./schema.js"; import { deploy } from "./utils.js"; const app = new Hono(); -const redisClient = createClient({ - url: process.env.REDIS_URL, + +// Initialize Inngest client +export const inngest = new Inngest({ + id: "dokploy-deployments", + name: "Dokploy Deployment Service", }); +export const deploymentFunction = inngest.createFunction( + { + id: "deploy-application", + name: "Deploy Application", + concurrency: [ + { + key: "event.data.serverId", + limit: 1, + }, + ], + retries: 0, + cancelOn: [ + { + event: "deployment/cancelled", + if: "async.data.applicationId == event.data.applicationId || async.data.composeId == event.data.composeId", + timeout: "1h", // Allow cancellation for up to 1 hour + }, + ], + }, + { event: "deployment/requested" }, + + async ({ event, step }) => { + const jobData = event.data as DeployJob; + + return await step.run("execute-deployment", async () => { + logger.info("Deploying started"); + + try { + const result = await deploy(jobData); + logger.info("Deployment finished", result); + + // Send success event + await inngest.send({ + name: "deployment/completed", + data: { + ...jobData, + result, + status: "success", + }, + }); + + return result; + } catch (error) { + logger.error("Deployment failed", { jobData, error }); + + // Send failure event + await inngest.send({ + name: "deployment/failed", + data: { + ...jobData, + error: error instanceof Error ? error.message : String(error), + status: "failed", + }, + }); + + throw error; + } + }); + }, +); + app.use(async (c, next) => { - if (c.req.path === "/health") { + if (c.req.path === "/health" || c.req.path === "/api/inngest") { return next(); } + const authHeader = c.req.header("X-API-Key"); if (process.env.API_KEY !== authHeader) { @@ -26,36 +95,97 @@ app.use(async (c, next) => { return next(); }); -app.post("/deploy", zValidator("json", deployJobSchema), (c) => { +app.post("/deploy", zValidator("json", deployJobSchema), async (c) => { const data = c.req.valid("json"); - queue.add(data, { groupName: data.serverId }); - return c.json( - { - message: "Deployment Added", - }, - 200, - ); -}); + logger.info("Received deployment request", data); -app.get("/health", async (c) => { - return c.json({ status: "ok" }); + try { + // Send event to Inngest instead of adding to Redis queue + await inngest.send({ + name: "deployment/requested", + data, + }); + + logger.info("Deployment event sent to Inngest", { + serverId: data.serverId, + }); + + return c.json( + { + message: "Deployment Added to Inngest Queue", + serverId: data.serverId, + }, + 200, + ); + } catch (error) { + console.log("error", error); + logger.error("Failed to send deployment event", error); + return c.json( + { + message: "Failed to queue deployment", + error: error instanceof Error ? error.message : String(error), + }, + 500, + ); + } }); -const queue = new Queue({ - name: "deployments", - process: async (job: DeployJob) => { - logger.info("Deploying job", job); - return await deploy(job); +app.post( + "/cancel-deployment", + zValidator("json", cancelDeploymentSchema), + async (c) => { + const data = c.req.valid("json"); + logger.info("Received cancel deployment request", data); + + try { + // Send cancellation event to Inngest + + await inngest.send({ + name: "deployment/cancelled", + data, + }); + + const identifier = + data.applicationType === "application" + ? `applicationId: ${data.applicationId}` + : `composeId: ${data.composeId}`; + + logger.info("Deployment cancellation event sent", { + ...data, + identifier, + }); + + return c.json({ + message: "Deployment cancellation requested", + applicationType: data.applicationType, + }); + } catch (error) { + logger.error("Failed to send deployment cancellation event", error); + return c.json( + { + message: "Failed to cancel deployment", + error: error instanceof Error ? error.message : String(error), + }, + 500, + ); + } }, - redisClient, +); + +app.get("/health", async (c) => { + return c.json({ status: "ok" }); }); -(async () => { - await redisClient.connect(); - await redisClient.flushAll(); - logger.info("Redis Cleaned"); -})(); +// Serve Inngest functions endpoint +app.on( + ["GET", "POST", "PUT"], + "/api/inngest", + serveInngest({ + client: inngest, + functions: [deploymentFunction], + }), +); const port = Number.parseInt(process.env.PORT || "3000"); -logger.info("Starting Deployments Server โœ…", port); +logger.info("Starting Deployments Server with Inngest โœ…", port); serve({ fetch: app.fetch, port }); diff --git a/apps/api/src/schema.ts b/apps/api/src/schema.ts index 609289bf70..5a4355956f 100644 --- a/apps/api/src/schema.ts +++ b/apps/api/src/schema.ts @@ -3,8 +3,8 @@ import { z } from "zod"; export const deployJobSchema = z.discriminatedUnion("applicationType", [ z.object({ applicationId: z.string(), - titleLog: z.string(), - descriptionLog: z.string(), + titleLog: z.string().optional(), + descriptionLog: z.string().optional(), server: z.boolean().optional(), type: z.enum(["deploy", "redeploy"]), applicationType: z.literal("application"), @@ -12,8 +12,8 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [ }), z.object({ composeId: z.string(), - titleLog: z.string(), - descriptionLog: z.string(), + titleLog: z.string().optional(), + descriptionLog: z.string().optional(), server: z.boolean().optional(), type: z.enum(["deploy", "redeploy"]), applicationType: z.literal("compose"), @@ -22,8 +22,8 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [ z.object({ applicationId: z.string(), previewDeploymentId: z.string(), - titleLog: z.string(), - descriptionLog: z.string(), + titleLog: z.string().optional(), + descriptionLog: z.string().optional(), server: z.boolean().optional(), type: z.enum(["deploy"]), applicationType: z.literal("application-preview"), @@ -32,3 +32,16 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [ ]); export type DeployJob = z.infer; + +export const cancelDeploymentSchema = z.discriminatedUnion("applicationType", [ + z.object({ + applicationId: z.string(), + applicationType: z.literal("application"), + }), + z.object({ + composeId: z.string(), + applicationType: z.literal("compose"), + }), +]); + +export type CancelDeploymentJob = z.infer; diff --git a/apps/api/src/utils.ts b/apps/api/src/utils.ts index 3f3c9698b6..ee2ac3e508 100644 --- a/apps/api/src/utils.ts +++ b/apps/api/src/utils.ts @@ -18,14 +18,14 @@ export const deploy = async (job: DeployJob) => { if (job.type === "redeploy") { await rebuildRemoteApplication({ applicationId: job.applicationId, - titleLog: job.titleLog, - descriptionLog: job.descriptionLog, + titleLog: job.titleLog || "Rebuild deployment", + descriptionLog: job.descriptionLog || "", }); } else if (job.type === "deploy") { await deployRemoteApplication({ applicationId: job.applicationId, - titleLog: job.titleLog, - descriptionLog: job.descriptionLog, + titleLog: job.titleLog || "Manual deployment", + descriptionLog: job.descriptionLog || "", }); } } @@ -38,14 +38,14 @@ export const deploy = async (job: DeployJob) => { if (job.type === "redeploy") { await rebuildRemoteCompose({ composeId: job.composeId, - titleLog: job.titleLog, - descriptionLog: job.descriptionLog, + titleLog: job.titleLog || "Rebuild deployment", + descriptionLog: job.descriptionLog || "", }); } else if (job.type === "deploy") { await deployRemoteCompose({ composeId: job.composeId, - titleLog: job.titleLog, - descriptionLog: job.descriptionLog, + titleLog: job.titleLog || "Manual deployment", + descriptionLog: job.descriptionLog || "", }); } } @@ -57,14 +57,14 @@ export const deploy = async (job: DeployJob) => { if (job.type === "deploy") { await deployRemotePreviewApplication({ applicationId: job.applicationId, - titleLog: job.titleLog, - descriptionLog: job.descriptionLog, + titleLog: job.titleLog || "Preview Deployment", + descriptionLog: job.descriptionLog || "", previewDeploymentId: job.previewDeploymentId, }); } } } - } catch (_) { + } catch (e) { if (job.applicationType === "application") { await updateApplicationStatus(job.applicationId, "error"); } else if (job.applicationType === "compose") { @@ -76,6 +76,8 @@ export const deploy = async (job: DeployJob) => { previewStatus: "error", }); } + + throw e; } return true; diff --git a/apps/dokploy/LICENSE.MD b/apps/dokploy/LICENSE.MD deleted file mode 100644 index 8a508efb41..0000000000 --- a/apps/dokploy/LICENSE.MD +++ /dev/null @@ -1,26 +0,0 @@ -# License - -## Core License (Apache License 2.0) - -Copyright 2024 Mauricio Siu. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and limitations under the License. - -## Additional Terms for Specific Features - -The following additional terms apply to the multi-node support, Docker Compose file, Preview Deployments 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, Docker Compose file support, Preview Deployments 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, Preview Deployments 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, Preview Deployments 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__/compose/compose.test.ts b/apps/dokploy/__test__/compose/compose.test.ts index 9d4ba20f5a..b691537a10 100644 --- a/apps/dokploy/__test__/compose/compose.test.ts +++ b/apps/dokploy/__test__/compose/compose.test.ts @@ -1,7 +1,7 @@ -import { addSuffixToAllProperties } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; -import { load } from "js-yaml"; +import { addSuffixToAllProperties } from "@dokploy/server"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFile1 = ` version: "3.8" @@ -61,7 +61,7 @@ secrets: file: ./db_password.txt `; -const expectedComposeFile1 = load(` +const expectedComposeFile1 = parse(` version: "3.8" services: @@ -120,7 +120,7 @@ secrets: `) as ComposeSpecification; test("Add suffix to all properties in compose file 1", () => { - const composeData = load(composeFile1) as ComposeSpecification; + const composeData = parse(composeFile1) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllProperties(composeData, suffix); @@ -185,7 +185,7 @@ secrets: file: ./db_password.txt `; -const expectedComposeFile2 = load(` +const expectedComposeFile2 = parse(` version: "3.8" services: @@ -243,7 +243,7 @@ secrets: `) as ComposeSpecification; test("Add suffix to all properties in compose file 2", () => { - const composeData = load(composeFile2) as ComposeSpecification; + const composeData = parse(composeFile2) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllProperties(composeData, suffix); @@ -308,7 +308,7 @@ secrets: file: ./service_secret.txt `; -const expectedComposeFile3 = load(` +const expectedComposeFile3 = parse(` version: "3.8" services: @@ -366,7 +366,7 @@ secrets: `) as ComposeSpecification; test("Add suffix to all properties in compose file 3", () => { - const composeData = load(composeFile3) as ComposeSpecification; + const composeData = parse(composeFile3) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllProperties(composeData, suffix); @@ -420,7 +420,7 @@ volumes: driver: local `; -const expectedComposeFile = load(` +const expectedComposeFile = parse(` version: "3.8" services: @@ -467,7 +467,7 @@ volumes: `) as ComposeSpecification; test("Add suffix to all properties in Plausible compose file", () => { - const composeData = load(composeFile) as ComposeSpecification; + const composeData = parse(composeFile) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllProperties(composeData, suffix); diff --git a/apps/dokploy/__test__/compose/config/config-root.test.ts b/apps/dokploy/__test__/compose/config/config-root.test.ts index 4b40c073ee..a633bab53b 100644 --- a/apps/dokploy/__test__/compose/config/config-root.test.ts +++ b/apps/dokploy/__test__/compose/config/config-root.test.ts @@ -1,8 +1,7 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToConfigsRoot } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; -import { load } from "js-yaml"; +import { addSuffixToConfigsRoot, generateRandomHash } from "@dokploy/server"; import { expect, test } from "vitest"; +import { parse } from "yaml"; test("Generate random hash with 8 characters", () => { const hash = generateRandomHash(); @@ -24,7 +23,7 @@ configs: `; test("Add suffix to configs in root property", () => { - const composeData = load(composeFile) as ComposeSpecification; + const composeData = parse(composeFile) as ComposeSpecification; const suffix = generateRandomHash(); @@ -60,7 +59,7 @@ configs: `; test("Add suffix to multiple configs in root property", () => { - const composeData = load(composeFileMultipleConfigs) as ComposeSpecification; + const composeData = parse(composeFileMultipleConfigs) as ComposeSpecification; const suffix = generateRandomHash(); @@ -93,7 +92,7 @@ configs: `; test("Add suffix to configs with different properties in root property", () => { - const composeData = load( + const composeData = parse( composeFileDifferentProperties, ) as ComposeSpecification; @@ -138,7 +137,7 @@ configs: `; // Expected compose file con el prefijo `testhash` -const expectedComposeFileConfigRoot = load(` +const expectedComposeFileConfigRoot = parse(` version: "3.8" services: @@ -163,7 +162,7 @@ configs: `) as ComposeSpecification; test("Add suffix to configs in root property", () => { - const composeData = load(composeFileConfigRoot) as ComposeSpecification; + const composeData = parse(composeFileConfigRoot) as ComposeSpecification; const suffix = "testhash"; diff --git a/apps/dokploy/__test__/compose/config/config-service.test.ts b/apps/dokploy/__test__/compose/config/config-service.test.ts index de014eb5e9..08dd696e63 100644 --- a/apps/dokploy/__test__/compose/config/config-service.test.ts +++ b/apps/dokploy/__test__/compose/config/config-service.test.ts @@ -1,8 +1,10 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToConfigsInServices } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; -import { load } from "js-yaml"; +import { + addSuffixToConfigsInServices, + generateRandomHash, +} from "@dokploy/server"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFile = ` version: "3.8" @@ -20,7 +22,7 @@ configs: `; test("Add suffix to configs in services", () => { - const composeData = load(composeFile) as ComposeSpecification; + const composeData = parse(composeFile) as ComposeSpecification; const suffix = generateRandomHash(); @@ -52,7 +54,7 @@ configs: `; test("Add suffix to configs in services with single config", () => { - const composeData = load( + const composeData = parse( composeFileSingleServiceConfig, ) as ComposeSpecification; @@ -106,7 +108,7 @@ configs: `; test("Add suffix to configs in services with multiple configs", () => { - const composeData = load( + const composeData = parse( composeFileMultipleServicesConfigs, ) as ComposeSpecification; @@ -155,7 +157,7 @@ services: `; // Expected compose file con el prefijo `testhash` -const expectedComposeFileConfigServices = load(` +const expectedComposeFileConfigServices = parse(` version: "3.8" services: @@ -180,7 +182,7 @@ services: `) as ComposeSpecification; test("Add suffix to configs in services", () => { - const composeData = load(composeFileConfigServices) as ComposeSpecification; + const composeData = parse(composeFileConfigServices) as ComposeSpecification; const suffix = "testhash"; diff --git a/apps/dokploy/__test__/compose/config/config.test.ts b/apps/dokploy/__test__/compose/config/config.test.ts index aed3350f5b..3a160431e8 100644 --- a/apps/dokploy/__test__/compose/config/config.test.ts +++ b/apps/dokploy/__test__/compose/config/config.test.ts @@ -1,8 +1,7 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToAllConfigs } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; -import { load } from "js-yaml"; +import { addSuffixToAllConfigs, generateRandomHash } from "@dokploy/server"; import { expect, test } from "vitest"; +import { parse } from "yaml"; test("Generate random hash with 8 characters", () => { const hash = generateRandomHash(); @@ -44,7 +43,7 @@ configs: file: ./db-config.yml `; -const expectedComposeFileCombinedConfigs = load(` +const expectedComposeFileCombinedConfigs = parse(` version: "3.8" services: @@ -78,7 +77,7 @@ configs: `) as ComposeSpecification; test("Add suffix to all configs in root and services", () => { - const composeData = load(composeFileCombinedConfigs) as ComposeSpecification; + const composeData = parse(composeFileCombinedConfigs) as ComposeSpecification; const suffix = "testhash"; @@ -123,7 +122,7 @@ configs: file: ./db-config.yml `; -const expectedComposeFileWithEnvAndExternal = load(` +const expectedComposeFileWithEnvAndExternal = parse(` version: "3.8" services: @@ -160,7 +159,7 @@ configs: `) as ComposeSpecification; test("Add suffix to configs with environment and external", () => { - const composeData = load( + const composeData = parse( composeFileWithEnvAndExternal, ) as ComposeSpecification; @@ -201,7 +200,7 @@ configs: file: ./db-config.yml `; -const expectedComposeFileWithTemplateDriverAndLabels = load(` +const expectedComposeFileWithTemplateDriverAndLabels = parse(` version: "3.8" services: @@ -232,7 +231,7 @@ configs: `) as ComposeSpecification; test("Add suffix to configs with template driver and labels", () => { - const composeData = load( + const composeData = parse( composeFileWithTemplateDriverAndLabels, ) as ComposeSpecification; diff --git a/apps/dokploy/__test__/compose/domain/labels.test.ts b/apps/dokploy/__test__/compose/domain/labels.test.ts index c5f45810f9..9a75e0a845 100644 --- a/apps/dokploy/__test__/compose/domain/labels.test.ts +++ b/apps/dokploy/__test__/compose/domain/labels.test.ts @@ -19,6 +19,8 @@ describe("createDomainLabels", () => { path: "/", createdAt: "", previewDeploymentId: "", + internalPath: "/", + stripPath: false, }; it("should create basic labels for web entrypoint", async () => { @@ -106,4 +108,136 @@ describe("createDomainLabels", () => { "traefik.http.services.test-app-1-web.loadbalancer.server.port=3000", ); }); + + it("should add stripPath middleware when stripPath is enabled", async () => { + const stripPathDomain = { + ...baseDomain, + path: "/api", + stripPath: true, + }; + const labels = await createDomainLabels(appName, stripPathDomain, "web"); + + expect(labels).toContain( + "traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api", + ); + expect(labels).toContain( + "traefik.http.routers.test-app-1-web.middlewares=stripprefix-test-app-1", + ); + }); + + it("should add internalPath middleware when internalPath is set", async () => { + const internalPathDomain = { + ...baseDomain, + internalPath: "/hello", + }; + const webLabels = await createDomainLabels( + appName, + internalPathDomain, + "web", + ); + const websecureLabels = await createDomainLabels( + appName, + internalPathDomain, + "websecure", + ); + + // Middleware definition should only appear in web entrypoint + expect(webLabels).toContain( + "traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello", + ); + expect(websecureLabels).not.toContain( + "traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello", + ); + + // Both routers should reference the middleware + expect(webLabels).toContain( + "traefik.http.routers.test-app-1-web.middlewares=addprefix-test-app-1", + ); + expect(websecureLabels).toContain( + "traefik.http.routers.test-app-1-websecure.middlewares=addprefix-test-app-1", + ); + }); + + it("should combine HTTPS redirect with internalPath middleware in correct order", async () => { + const combinedDomain = { + ...baseDomain, + https: true, + internalPath: "/hello", + }; + const webLabels = await createDomainLabels(appName, combinedDomain, "web"); + const websecureLabels = await createDomainLabels( + appName, + combinedDomain, + "websecure", + ); + + // Web entrypoint should have both middlewares with redirect first + expect(webLabels).toContain( + "traefik.http.routers.test-app-1-web.middlewares=redirect-to-https@file,addprefix-test-app-1", + ); + + // Websecure should only have the addprefix middleware + expect(websecureLabels).toContain( + "traefik.http.routers.test-app-1-websecure.middlewares=addprefix-test-app-1", + ); + + // Middleware definition should only appear once (in web) + expect(webLabels).toContain( + "traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello", + ); + expect(websecureLabels).not.toContain( + "traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello", + ); + }); + + it("should combine all middlewares in correct order", async () => { + const fullDomain = { + ...baseDomain, + https: true, + path: "/api", + stripPath: true, + internalPath: "/hello", + }; + const webLabels = await createDomainLabels(appName, fullDomain, "web"); + + // Should have all middleware definitions (only in web) + expect(webLabels).toContain( + "traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api", + ); + expect(webLabels).toContain( + "traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello", + ); + + // Should have middlewares in correct order: redirect, stripprefix, addprefix + expect(webLabels).toContain( + "traefik.http.routers.test-app-1-web.middlewares=redirect-to-https@file,stripprefix-test-app-1,addprefix-test-app-1", + ); + }); + + it("should not add middleware definitions for websecure entrypoint", async () => { + const internalPathDomain = { + ...baseDomain, + path: "/api", + stripPath: true, + internalPath: "/hello", + }; + const websecureLabels = await createDomainLabels( + appName, + internalPathDomain, + "websecure", + ); + + // Should not contain any middleware definitions + expect(websecureLabels).not.toContain( + "traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api", + ); + expect(websecureLabels).not.toContain( + "traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello", + ); + + // But should reference the middlewares + expect(websecureLabels).toContain( + "traefik.http.routers.test-app-1-websecure.middlewares=stripprefix-test-app-1,addprefix-test-app-1", + ); + }); }); diff --git a/apps/dokploy/__test__/compose/network/network-root.test.ts b/apps/dokploy/__test__/compose/network/network-root.test.ts index 980502fff8..0d3c841d40 100644 --- a/apps/dokploy/__test__/compose/network/network-root.test.ts +++ b/apps/dokploy/__test__/compose/network/network-root.test.ts @@ -1,8 +1,7 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToNetworksRoot } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; -import { load } from "js-yaml"; +import { addSuffixToNetworksRoot, generateRandomHash } from "@dokploy/server"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFile = ` version: "3.8" @@ -36,7 +35,7 @@ test("Generate random hash with 8 characters", () => { }); test("Add suffix to networks root property", () => { - const composeData = load(composeFile) as ComposeSpecification; + const composeData = parse(composeFile) as ComposeSpecification; const suffix = generateRandomHash(); @@ -80,7 +79,7 @@ networks: `; test("Add suffix to advanced networks root property (2 TRY)", () => { - const composeData = load(composeFile2) as ComposeSpecification; + const composeData = parse(composeFile2) as ComposeSpecification; const suffix = generateRandomHash(); @@ -121,7 +120,7 @@ networks: `; test("Add suffix to networks with external properties", () => { - const composeData = load(composeFile3) as ComposeSpecification; + const composeData = parse(composeFile3) as ComposeSpecification; const suffix = generateRandomHash(); @@ -161,7 +160,7 @@ networks: `; test("Add suffix to networks with IPAM configurations", () => { - const composeData = load(composeFile4) as ComposeSpecification; + const composeData = parse(composeFile4) as ComposeSpecification; const suffix = generateRandomHash(); @@ -202,7 +201,7 @@ networks: `; test("Add suffix to networks with custom options", () => { - const composeData = load(composeFile5) as ComposeSpecification; + const composeData = parse(composeFile5) as ComposeSpecification; const suffix = generateRandomHash(); @@ -265,7 +264,7 @@ networks: `; test("Add suffix to networks with static suffix", () => { - const composeData = load(composeFile6) as ComposeSpecification; + const composeData = parse(composeFile6) as ComposeSpecification; const suffix = "testhash"; @@ -274,7 +273,7 @@ test("Add suffix to networks with static suffix", () => { } const networks = addSuffixToNetworksRoot(composeData.networks, suffix); - const expectedComposeData = load( + const expectedComposeData = parse( expectedComposeFile6, ) as ComposeSpecification; expect(networks).toStrictEqual(expectedComposeData.networks); @@ -294,7 +293,7 @@ networks: `; test("It shoudn't add suffix to dokploy-network", () => { - const composeData = load(composeFile7) as ComposeSpecification; + const composeData = parse(composeFile7) as ComposeSpecification; const suffix = generateRandomHash(); diff --git a/apps/dokploy/__test__/compose/network/network-service.test.ts b/apps/dokploy/__test__/compose/network/network-service.test.ts index ee07d9de96..e07fa15465 100644 --- a/apps/dokploy/__test__/compose/network/network-service.test.ts +++ b/apps/dokploy/__test__/compose/network/network-service.test.ts @@ -1,8 +1,10 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToServiceNetworks } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; -import { load } from "js-yaml"; +import { + addSuffixToServiceNetworks, + generateRandomHash, +} from "@dokploy/server"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFile = ` version: "3.8" @@ -21,7 +23,7 @@ services: `; test("Add suffix to networks in services", () => { - const composeData = load(composeFile) as ComposeSpecification; + const composeData = parse(composeFile) as ComposeSpecification; const suffix = generateRandomHash(); @@ -65,7 +67,7 @@ networks: `; test("Add suffix to networks in services with aliases", () => { - const composeData = load(composeFile2) as ComposeSpecification; + const composeData = parse(composeFile2) as ComposeSpecification; const suffix = generateRandomHash(); @@ -105,7 +107,7 @@ networks: `; test("Add suffix to networks in services (Object with simple networks)", () => { - const composeData = load(composeFile3) as ComposeSpecification; + const composeData = parse(composeFile3) as ComposeSpecification; const suffix = generateRandomHash(); @@ -151,7 +153,7 @@ networks: `; test("Add suffix to networks in services (combined case)", () => { - const composeData = load(composeFileCombined) as ComposeSpecification; + const composeData = parse(composeFileCombined) as ComposeSpecification; const suffix = generateRandomHash(); @@ -194,7 +196,7 @@ services: `; test("It shoudn't add suffix to dokploy-network in services", () => { - const composeData = load(composeFile7) as ComposeSpecification; + const composeData = parse(composeFile7) as ComposeSpecification; const suffix = generateRandomHash(); @@ -243,7 +245,7 @@ services: `; test("It shoudn't add suffix to dokploy-network in services multiples cases", () => { - const composeData = load(composeFile8) as ComposeSpecification; + const composeData = parse(composeFile8) as ComposeSpecification; const suffix = generateRandomHash(); diff --git a/apps/dokploy/__test__/compose/network/network.test.ts b/apps/dokploy/__test__/compose/network/network.test.ts index 39cf039588..c1900ed74e 100644 --- a/apps/dokploy/__test__/compose/network/network.test.ts +++ b/apps/dokploy/__test__/compose/network/network.test.ts @@ -1,12 +1,12 @@ -import { generateRandomHash } from "@dokploy/server"; +import type { ComposeSpecification } from "@dokploy/server"; import { addSuffixToAllNetworks, + addSuffixToNetworksRoot, addSuffixToServiceNetworks, + generateRandomHash, } from "@dokploy/server"; -import { addSuffixToNetworksRoot } from "@dokploy/server"; -import type { ComposeSpecification } from "@dokploy/server"; -import { load } from "js-yaml"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFileCombined = ` version: "3.8" @@ -39,7 +39,7 @@ networks: `; test("Add suffix to networks in services and root (combined case)", () => { - const composeData = load(composeFileCombined) as ComposeSpecification; + const composeData = parse(composeFileCombined) as ComposeSpecification; const suffix = generateRandomHash(); @@ -89,7 +89,7 @@ test("Add suffix to networks in services and root (combined case)", () => { expect(redisNetworks).not.toHaveProperty("backend"); }); -const expectedComposeFile = load(` +const expectedComposeFile = parse(` version: "3.8" services: @@ -120,7 +120,7 @@ networks: `); test("Add suffix to networks in compose file", () => { - const composeData = load(composeFileCombined) as ComposeSpecification; + const composeData = parse(composeFileCombined) as ComposeSpecification; const suffix = "testhash"; if (!composeData?.networks) { @@ -156,7 +156,7 @@ networks: driver: bridge `; -const expectedComposeFile2 = load(` +const expectedComposeFile2 = parse(` version: "3.8" services: @@ -182,7 +182,7 @@ networks: `); test("Add suffix to networks in compose file with external and internal networks", () => { - const composeData = load(composeFile2) as ComposeSpecification; + const composeData = parse(composeFile2) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllNetworks(composeData, suffix); @@ -218,7 +218,7 @@ networks: com.docker.network.bridge.enable_icc: "true" `; -const expectedComposeFile3 = load(` +const expectedComposeFile3 = parse(` version: "3.8" services: @@ -247,7 +247,7 @@ networks: `); test("Add suffix to networks in compose file with multiple services and complex network configurations", () => { - const composeData = load(composeFile3) as ComposeSpecification; + const composeData = parse(composeFile3) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllNetworks(composeData, suffix); @@ -289,7 +289,7 @@ networks: `; -const expectedComposeFile4 = load(` +const expectedComposeFile4 = parse(` version: "3.8" services: @@ -326,7 +326,7 @@ networks: `); test("Expect don't add suffix to dokploy-network in compose file with multiple services and complex network configurations", () => { - const composeData = load(composeFile4) as ComposeSpecification; + const composeData = parse(composeFile4) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllNetworks(composeData, suffix); diff --git a/apps/dokploy/__test__/compose/secrets/secret-root.test.ts b/apps/dokploy/__test__/compose/secrets/secret-root.test.ts index 1b1898c59c..ef74d64cf3 100644 --- a/apps/dokploy/__test__/compose/secrets/secret-root.test.ts +++ b/apps/dokploy/__test__/compose/secrets/secret-root.test.ts @@ -1,8 +1,7 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToSecretsRoot } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; -import { load } from "js-yaml"; +import { addSuffixToSecretsRoot, generateRandomHash } from "@dokploy/server"; import { expect, test } from "vitest"; +import { parse } from "yaml"; test("Generate random hash with 8 characters", () => { const hash = generateRandomHash(); @@ -24,7 +23,7 @@ secrets: `; test("Add suffix to secrets in root property", () => { - const composeData = load(composeFileSecretsRoot) as ComposeSpecification; + const composeData = parse(composeFileSecretsRoot) as ComposeSpecification; const suffix = generateRandomHash(); if (!composeData?.secrets) { @@ -53,7 +52,7 @@ secrets: `; test("Add suffix to secrets in root property (Test 1)", () => { - const composeData = load(composeFileSecretsRoot1) as ComposeSpecification; + const composeData = parse(composeFileSecretsRoot1) as ComposeSpecification; const suffix = generateRandomHash(); if (!composeData?.secrets) { @@ -85,7 +84,7 @@ secrets: `; test("Add suffix to secrets in root property (Test 2)", () => { - const composeData = load(composeFileSecretsRoot2) as ComposeSpecification; + const composeData = parse(composeFileSecretsRoot2) as ComposeSpecification; const suffix = generateRandomHash(); if (!composeData?.secrets) { diff --git a/apps/dokploy/__test__/compose/secrets/secret-services.test.ts b/apps/dokploy/__test__/compose/secrets/secret-services.test.ts index 5206bbbaf1..a378bd606a 100644 --- a/apps/dokploy/__test__/compose/secrets/secret-services.test.ts +++ b/apps/dokploy/__test__/compose/secrets/secret-services.test.ts @@ -1,8 +1,10 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToSecretsInServices } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; -import { load } from "js-yaml"; +import { + addSuffixToSecretsInServices, + generateRandomHash, +} from "@dokploy/server"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFileSecretsServices = ` version: "3.8" @@ -19,7 +21,7 @@ secrets: `; test("Add suffix to secrets in services", () => { - const composeData = load(composeFileSecretsServices) as ComposeSpecification; + const composeData = parse(composeFileSecretsServices) as ComposeSpecification; const suffix = generateRandomHash(); if (!composeData.services) { @@ -52,7 +54,9 @@ secrets: `; test("Add suffix to secrets in services (Test 1)", () => { - const composeData = load(composeFileSecretsServices1) as ComposeSpecification; + const composeData = parse( + composeFileSecretsServices1, + ) as ComposeSpecification; const suffix = generateRandomHash(); if (!composeData.services) { @@ -91,7 +95,9 @@ secrets: `; test("Add suffix to secrets in services (Test 2)", () => { - const composeData = load(composeFileSecretsServices2) as ComposeSpecification; + const composeData = parse( + composeFileSecretsServices2, + ) as ComposeSpecification; const suffix = generateRandomHash(); if (!composeData.services) { diff --git a/apps/dokploy/__test__/compose/secrets/secret.test.ts b/apps/dokploy/__test__/compose/secrets/secret.test.ts index d874dc5e78..3f6544bf1f 100644 --- a/apps/dokploy/__test__/compose/secrets/secret.test.ts +++ b/apps/dokploy/__test__/compose/secrets/secret.test.ts @@ -1,7 +1,7 @@ -import { addSuffixToAllSecrets } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; -import { load } from "js-yaml"; +import { addSuffixToAllSecrets } from "@dokploy/server"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFileCombinedSecrets = ` version: "3.8" @@ -25,7 +25,7 @@ secrets: file: ./app_secret.txt `; -const expectedComposeFileCombinedSecrets = load(` +const expectedComposeFileCombinedSecrets = parse(` version: "3.8" services: @@ -48,7 +48,7 @@ secrets: `) as ComposeSpecification; test("Add suffix to all secrets", () => { - const composeData = load(composeFileCombinedSecrets) as ComposeSpecification; + const composeData = parse(composeFileCombinedSecrets) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllSecrets(composeData, suffix); @@ -77,7 +77,7 @@ secrets: file: ./cache_secret.txt `; -const expectedComposeFileCombinedSecrets3 = load(` +const expectedComposeFileCombinedSecrets3 = parse(` version: "3.8" services: @@ -99,7 +99,9 @@ secrets: `) as ComposeSpecification; test("Add suffix to all secrets (3rd Case)", () => { - const composeData = load(composeFileCombinedSecrets3) as ComposeSpecification; + const composeData = parse( + composeFileCombinedSecrets3, + ) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllSecrets(composeData, suffix); @@ -128,7 +130,7 @@ secrets: file: ./db_password.txt `; -const expectedComposeFileCombinedSecrets4 = load(` +const expectedComposeFileCombinedSecrets4 = parse(` version: "3.8" services: @@ -150,7 +152,9 @@ secrets: `) as ComposeSpecification; test("Add suffix to all secrets (4th Case)", () => { - const composeData = load(composeFileCombinedSecrets4) as ComposeSpecification; + const composeData = parse( + composeFileCombinedSecrets4, + ) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllSecrets(composeData, suffix); diff --git a/apps/dokploy/__test__/compose/service/service-container-name.test.ts b/apps/dokploy/__test__/compose/service/service-container-name.test.ts index bcb51fd043..d6521464d0 100644 --- a/apps/dokploy/__test__/compose/service/service-container-name.test.ts +++ b/apps/dokploy/__test__/compose/service/service-container-name.test.ts @@ -1,8 +1,7 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToServiceNames } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; -import { load } from "js-yaml"; +import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFile = ` version: "3.8" @@ -28,7 +27,7 @@ test("Generate random hash with 8 characters", () => { }); test("Add suffix to service names with container_name in compose file", () => { - const composeData = load(composeFile) as ComposeSpecification; + const composeData = parse(composeFile) as ComposeSpecification; const suffix = generateRandomHash(); diff --git a/apps/dokploy/__test__/compose/service/service-depends-on.test.ts b/apps/dokploy/__test__/compose/service/service-depends-on.test.ts index b27414be53..547c309d53 100644 --- a/apps/dokploy/__test__/compose/service/service-depends-on.test.ts +++ b/apps/dokploy/__test__/compose/service/service-depends-on.test.ts @@ -1,8 +1,7 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToServiceNames } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; -import { load } from "js-yaml"; +import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { expect, test } from "vitest"; +import { parse } from "yaml"; test("Generate random hash with 8 characters", () => { const hash = generateRandomHash(); @@ -33,7 +32,7 @@ networks: `; test("Add suffix to service names with depends_on (array) in compose file", () => { - const composeData = load(composeFile4) as ComposeSpecification; + const composeData = parse(composeFile4) as ComposeSpecification; const suffix = generateRandomHash(); @@ -103,7 +102,7 @@ networks: `; test("Add suffix to service names with depends_on (object) in compose file", () => { - const composeData = load(composeFile5) as ComposeSpecification; + const composeData = parse(composeFile5) as ComposeSpecification; const suffix = generateRandomHash(); diff --git a/apps/dokploy/__test__/compose/service/service-extends.test.ts b/apps/dokploy/__test__/compose/service/service-extends.test.ts index 8309a32fd4..f539eeebd4 100644 --- a/apps/dokploy/__test__/compose/service/service-extends.test.ts +++ b/apps/dokploy/__test__/compose/service/service-extends.test.ts @@ -1,8 +1,7 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToServiceNames } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; -import { load } from "js-yaml"; +import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { expect, test } from "vitest"; +import { parse } from "yaml"; test("Generate random hash with 8 characters", () => { const hash = generateRandomHash(); @@ -31,7 +30,7 @@ networks: `; test("Add suffix to service names with extends (string) in compose file", () => { - const composeData = load(composeFile6) as ComposeSpecification; + const composeData = parse(composeFile6) as ComposeSpecification; const suffix = generateRandomHash(); @@ -91,7 +90,7 @@ networks: `; test("Add suffix to service names with extends (object) in compose file", () => { - const composeData = load(composeFile7) as ComposeSpecification; + const composeData = parse(composeFile7) as ComposeSpecification; const suffix = generateRandomHash(); diff --git a/apps/dokploy/__test__/compose/service/service-links.test.ts b/apps/dokploy/__test__/compose/service/service-links.test.ts index 5f9b01ab28..4187edce86 100644 --- a/apps/dokploy/__test__/compose/service/service-links.test.ts +++ b/apps/dokploy/__test__/compose/service/service-links.test.ts @@ -1,8 +1,7 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToServiceNames } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; -import { load } from "js-yaml"; +import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { expect, test } from "vitest"; +import { parse } from "yaml"; test("Generate random hash with 8 characters", () => { const hash = generateRandomHash(); @@ -32,7 +31,7 @@ networks: `; test("Add suffix to service names with links in compose file", () => { - const composeData = load(composeFile2) as ComposeSpecification; + const composeData = parse(composeFile2) as ComposeSpecification; const suffix = generateRandomHash(); diff --git a/apps/dokploy/__test__/compose/service/service-names.test.ts b/apps/dokploy/__test__/compose/service/service-names.test.ts index 936a32ecc2..c9c9d78c1a 100644 --- a/apps/dokploy/__test__/compose/service/service-names.test.ts +++ b/apps/dokploy/__test__/compose/service/service-names.test.ts @@ -1,8 +1,7 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToServiceNames } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; -import { load } from "js-yaml"; +import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { expect, test } from "vitest"; +import { parse } from "yaml"; test("Generate random hash with 8 characters", () => { const hash = generateRandomHash(); @@ -27,7 +26,7 @@ networks: `; test("Add suffix to service names in compose file", () => { - const composeData = load(composeFile) as ComposeSpecification; + const composeData = parse(composeFile) as ComposeSpecification; const suffix = generateRandomHash(); diff --git a/apps/dokploy/__test__/compose/service/service.test.ts b/apps/dokploy/__test__/compose/service/service.test.ts index c6050f75a2..a58e16722e 100644 --- a/apps/dokploy/__test__/compose/service/service.test.ts +++ b/apps/dokploy/__test__/compose/service/service.test.ts @@ -1,10 +1,10 @@ +import type { ComposeSpecification } from "@dokploy/server"; import { addSuffixToAllServiceNames, addSuffixToServiceNames, } from "@dokploy/server"; -import type { ComposeSpecification } from "@dokploy/server"; -import { load } from "js-yaml"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFileCombinedAllCases = ` version: "3.8" @@ -38,7 +38,7 @@ networks: driver: bridge `; -const expectedComposeFile = load(` +const expectedComposeFile = parse(` version: "3.8" services: @@ -71,7 +71,9 @@ networks: `); test("Add suffix to all service names in compose file", () => { - const composeData = load(composeFileCombinedAllCases) as ComposeSpecification; + const composeData = parse( + composeFileCombinedAllCases, + ) as ComposeSpecification; const suffix = "testhash"; @@ -131,7 +133,7 @@ networks: driver: bridge `; -const expectedComposeFile1 = load(` +const expectedComposeFile1 = parse(` version: "3.8" services: @@ -176,7 +178,7 @@ networks: `) as ComposeSpecification; test("Add suffix to all service names in compose file 1", () => { - const composeData = load(composeFile1) as ComposeSpecification; + const composeData = parse(composeFile1) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix); @@ -227,7 +229,7 @@ networks: driver: bridge `; -const expectedComposeFile2 = load(` +const expectedComposeFile2 = parse(` version: "3.8" services: @@ -271,7 +273,7 @@ networks: `) as ComposeSpecification; test("Add suffix to all service names in compose file 2", () => { - const composeData = load(composeFile2) as ComposeSpecification; + const composeData = parse(composeFile2) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix); @@ -322,7 +324,7 @@ networks: driver: bridge `; -const expectedComposeFile3 = load(` +const expectedComposeFile3 = parse(` version: "3.8" services: @@ -366,7 +368,7 @@ networks: `) as ComposeSpecification; test("Add suffix to all service names in compose file 3", () => { - const composeData = load(composeFile3) as ComposeSpecification; + const composeData = parse(composeFile3) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix); diff --git a/apps/dokploy/__test__/compose/service/sevice-volumes-from.test.ts b/apps/dokploy/__test__/compose/service/sevice-volumes-from.test.ts index 8066a6dd7d..1de94b894e 100644 --- a/apps/dokploy/__test__/compose/service/sevice-volumes-from.test.ts +++ b/apps/dokploy/__test__/compose/service/sevice-volumes-from.test.ts @@ -1,8 +1,7 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToServiceNames } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; -import { load } from "js-yaml"; +import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { expect, test } from "vitest"; +import { parse } from "yaml"; test("Generate random hash with 8 characters", () => { const hash = generateRandomHash(); @@ -36,7 +35,7 @@ networks: `; test("Add suffix to service names with volumes_from in compose file", () => { - const composeData = load(composeFile3) as ComposeSpecification; + const composeData = parse(composeFile3) as ComposeSpecification; const suffix = generateRandomHash(); diff --git a/apps/dokploy/__test__/compose/volume/volume-2.test.ts b/apps/dokploy/__test__/compose/volume/volume-2.test.ts index 61cba82d33..7ffbc4c1a4 100644 --- a/apps/dokploy/__test__/compose/volume/volume-2.test.ts +++ b/apps/dokploy/__test__/compose/volume/volume-2.test.ts @@ -1,8 +1,11 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToAllVolumes, addSuffixToVolumesRoot } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; -import { load } from "js-yaml"; +import { + addSuffixToAllVolumes, + addSuffixToVolumesRoot, + generateRandomHash, +} from "@dokploy/server"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFile = ` services: @@ -67,7 +70,7 @@ volumes: driver: local `; -const expectedDockerCompose = load(` +const expectedDockerCompose = parse(` services: mail: image: bytemark/smtp @@ -140,7 +143,7 @@ test("Generate random hash with 8 characters", () => { // Docker compose needs unique names for services, volumes, networks and containers // So base on a input which is a dockercompose file, it should replace the name with a hash and return a new dockercompose file test("Add suffix to volumes root property", () => { - const composeData = load(composeFile) as ComposeSpecification; + const composeData = parse(composeFile) as ComposeSpecification; const suffix = generateRandomHash(); @@ -162,7 +165,7 @@ test("Add suffix to volumes root property", () => { }); test("Expect to change the suffix in all the possible places", () => { - const composeData = load(composeFile) as ComposeSpecification; + const composeData = parse(composeFile) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllVolumes(composeData, suffix); @@ -192,7 +195,7 @@ volumes: mongo-data: `; -const expectedDockerCompose2 = load(` +const expectedDockerCompose2 = parse(` version: '3.8' services: app: @@ -215,7 +218,7 @@ volumes: `) as ComposeSpecification; test("Expect to change the suffix in all the possible places (2 Try)", () => { - const composeData = load(composeFile2) as ComposeSpecification; + const composeData = parse(composeFile2) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllVolumes(composeData, suffix); @@ -245,7 +248,7 @@ volumes: mongo-data: `; -const expectedDockerCompose3 = load(` +const expectedDockerCompose3 = parse(` version: '3.8' services: app: @@ -268,7 +271,7 @@ volumes: `) as ComposeSpecification; test("Expect to change the suffix in all the possible places (3 Try)", () => { - const composeData = load(composeFile3) as ComposeSpecification; + const composeData = parse(composeFile3) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllVolumes(composeData, suffix); @@ -642,7 +645,7 @@ volumes: db-config: `; -const expectedDockerComposeComplex = load(` +const expectedDockerComposeComplex = parse(` version: "3.8" services: studio: @@ -1009,7 +1012,7 @@ volumes: `); test("Expect to change the suffix in all the possible places (4 Try)", () => { - const composeData = load(composeFileComplex) as ComposeSpecification; + const composeData = parse(composeFileComplex) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllVolumes(composeData, suffix); @@ -1062,7 +1065,7 @@ volumes: db-data: `; -const expectedDockerComposeExample1 = load(` +const expectedDockerComposeExample1 = parse(` version: "3.8" services: web: @@ -1108,7 +1111,7 @@ volumes: `) as ComposeSpecification; test("Expect to change the suffix in all the possible places (5 Try)", () => { - const composeData = load(composeFileExample1) as ComposeSpecification; + const composeData = parse(composeFileExample1) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllVolumes(composeData, suffix); @@ -1140,7 +1143,7 @@ volumes: backrest-cache: `; -const expectedDockerComposeBackrest = load(` +const expectedDockerComposeBackrest = parse(` services: backrest: image: garethgeorge/backrest:v1.7.3 @@ -1165,7 +1168,7 @@ volumes: `) as ComposeSpecification; test("Should handle volume paths with subdirectories correctly", () => { - const composeData = load(composeFileBackrest) as ComposeSpecification; + const composeData = parse(composeFileBackrest) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllVolumes(composeData, suffix); diff --git a/apps/dokploy/__test__/compose/volume/volume-root.test.ts b/apps/dokploy/__test__/compose/volume/volume-root.test.ts index d91cb64d3a..69afb7f992 100644 --- a/apps/dokploy/__test__/compose/volume/volume-root.test.ts +++ b/apps/dokploy/__test__/compose/volume/volume-root.test.ts @@ -1,8 +1,7 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToVolumesRoot } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; -import { load } from "js-yaml"; +import { addSuffixToVolumesRoot, generateRandomHash } from "@dokploy/server"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFile = ` version: "3.8" @@ -30,7 +29,7 @@ test("Generate random hash with 8 characters", () => { }); test("Add suffix to volumes in root property", () => { - const composeData = load(composeFile) as ComposeSpecification; + const composeData = parse(composeFile) as ComposeSpecification; const suffix = generateRandomHash(); @@ -68,7 +67,7 @@ networks: `; test("Add suffix to volumes in root property (Case 2)", () => { - const composeData = load(composeFile2) as ComposeSpecification; + const composeData = parse(composeFile2) as ComposeSpecification; const suffix = generateRandomHash(); @@ -102,7 +101,7 @@ networks: `; test("Add suffix to volumes in root property (Case 3)", () => { - const composeData = load(composeFile3) as ComposeSpecification; + const composeData = parse(composeFile3) as ComposeSpecification; const suffix = generateRandomHash(); @@ -149,7 +148,7 @@ volumes: `; // Expected compose file con el prefijo `testhash` -const expectedComposeFile4 = load(` +const expectedComposeFile4 = parse(` version: "3.8" services: @@ -180,7 +179,7 @@ volumes: `) as ComposeSpecification; test("Add suffix to volumes in root property", () => { - const composeData = load(composeFile4) as ComposeSpecification; + const composeData = parse(composeFile4) as ComposeSpecification; const suffix = "testhash"; diff --git a/apps/dokploy/__test__/compose/volume/volume-services.test.ts b/apps/dokploy/__test__/compose/volume/volume-services.test.ts index 04a1a45ae8..a42ab5fa94 100644 --- a/apps/dokploy/__test__/compose/volume/volume-services.test.ts +++ b/apps/dokploy/__test__/compose/volume/volume-services.test.ts @@ -1,8 +1,10 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToVolumesInServices } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; -import { load } from "js-yaml"; +import { + addSuffixToVolumesInServices, + generateRandomHash, +} from "@dokploy/server"; import { expect, test } from "vitest"; +import { parse } from "yaml"; test("Generate random hash with 8 characters", () => { const hash = generateRandomHash(); @@ -22,7 +24,7 @@ services: `; test("Add suffix to volumes declared directly in services", () => { - const composeData = load(composeFile1) as ComposeSpecification; + const composeData = parse(composeFile1) as ComposeSpecification; const suffix = generateRandomHash(); @@ -57,7 +59,7 @@ volumes: `; test("Add suffix to volumes declared directly in services (Case 2)", () => { - const composeData = load(composeFileTypeVolume) as ComposeSpecification; + const composeData = parse(composeFileTypeVolume) as ComposeSpecification; const suffix = generateRandomHash(); diff --git a/apps/dokploy/__test__/compose/volume/volume.test.ts b/apps/dokploy/__test__/compose/volume/volume.test.ts index 6c43447623..2ccd12da69 100644 --- a/apps/dokploy/__test__/compose/volume/volume.test.ts +++ b/apps/dokploy/__test__/compose/volume/volume.test.ts @@ -1,7 +1,7 @@ -import { addSuffixToAllVolumes } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; -import { load } from "js-yaml"; +import { addSuffixToAllVolumes } from "@dokploy/server"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFileTypeVolume = ` version: "3.8" @@ -23,7 +23,7 @@ volumes: driver: local `; -const expectedComposeFileTypeVolume = load(` +const expectedComposeFileTypeVolume = parse(` version: "3.8" services: @@ -44,7 +44,7 @@ volumes: `) as ComposeSpecification; test("Add suffix to volumes with type: volume in services", () => { - const composeData = load(composeFileTypeVolume) as ComposeSpecification; + const composeData = parse(composeFileTypeVolume) as ComposeSpecification; const suffix = "testhash"; @@ -73,7 +73,7 @@ volumes: driver: local `; -const expectedComposeFileTypeVolume1 = load(` +const expectedComposeFileTypeVolume1 = parse(` version: "3.8" services: @@ -93,7 +93,7 @@ volumes: `) as ComposeSpecification; test("Add suffix to mixed volumes in services", () => { - const composeData = load(composeFileTypeVolume1) as ComposeSpecification; + const composeData = parse(composeFileTypeVolume1) as ComposeSpecification; const suffix = "testhash"; @@ -128,7 +128,7 @@ volumes: device: /path/to/app/logs `; -const expectedComposeFileTypeVolume2 = load(` +const expectedComposeFileTypeVolume2 = parse(` version: "3.8" services: @@ -154,7 +154,7 @@ volumes: `) as ComposeSpecification; test("Add suffix to complex volume configurations in services", () => { - const composeData = load(composeFileTypeVolume2) as ComposeSpecification; + const composeData = parse(composeFileTypeVolume2) as ComposeSpecification; const suffix = "testhash"; @@ -218,7 +218,7 @@ volumes: device: /path/to/shared/logs `; -const expectedComposeFileTypeVolume3 = load(` +const expectedComposeFileTypeVolume3 = parse(` version: "3.8" services: @@ -273,7 +273,7 @@ volumes: `) as ComposeSpecification; test("Add suffix to complex nested volumes configuration in services", () => { - const composeData = load(composeFileTypeVolume3) as ComposeSpecification; + const composeData = parse(composeFileTypeVolume3) as ComposeSpecification; const suffix = "testhash"; diff --git a/apps/dokploy/__test__/deploy/github.test.ts b/apps/dokploy/__test__/deploy/github.test.ts index 18d7619abf..03805b08d0 100644 --- a/apps/dokploy/__test__/deploy/github.test.ts +++ b/apps/dokploy/__test__/deploy/github.test.ts @@ -1,5 +1,5 @@ -import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]"; import { describe, expect, it } from "vitest"; +import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]"; describe("GitHub Webhook Skip CI", () => { const mockGithubHeaders = { diff --git a/apps/dokploy/__test__/drop/drop.test.test.ts b/apps/dokploy/__test__/drop/drop.test.ts similarity index 94% rename from apps/dokploy/__test__/drop/drop.test.test.ts rename to apps/dokploy/__test__/drop/drop.test.ts index 9fa68b6bb2..b597b3aa47 100644 --- a/apps/dokploy/__test__/drop/drop.test.test.ts +++ b/apps/dokploy/__test__/drop/drop.test.ts @@ -1,12 +1,12 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { paths } from "@dokploy/server/constants"; -const { APPLICATIONS_PATH } = paths(); import type { ApplicationNested } from "@dokploy/server"; import { unzipDrop } from "@dokploy/server"; +import { paths } from "@dokploy/server/constants"; import AdmZip from "adm-zip"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +const { APPLICATIONS_PATH } = paths(); vi.mock("@dokploy/server/constants", async (importOriginal) => { const actual = await importOriginal(); return { @@ -25,10 +25,13 @@ if (typeof window === "undefined") { } const baseApp: ApplicationNested = { + railpackVersion: "0.2.2", applicationId: "", + previewLabels: [], herokuVersion: "", giteaBranch: "", giteaBuildPath: "", + previewRequireCollaboratorPermissions: false, giteaId: "", giteaOwner: "", giteaRepository: "", @@ -53,13 +56,21 @@ const baseApp: ApplicationNested = { previewPort: 3000, previewLimit: 0, previewWildcard: "", - project: { + environment: { env: "", - organizationId: "", + environmentId: "", name: "", - description: "", createdAt: "", + description: "", projectId: "", + project: { + env: "", + organizationId: "", + name: "", + description: "", + createdAt: "", + projectId: "", + }, }, buildArgs: null, buildPath: "/", @@ -89,6 +100,7 @@ const baseApp: ApplicationNested = { dockerfile: null, dockerImage: null, dropBuildPath: null, + environmentId: "", enabled: null, env: null, healthCheckSwarm: null, @@ -103,7 +115,6 @@ const baseApp: ApplicationNested = { password: null, placementSwarm: null, ports: [], - projectId: "", publishDirectory: null, isStaticSpa: null, redirects: [], @@ -141,7 +152,7 @@ describe("unzipDrop using real zip files", () => { const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); const zip = new AdmZip("./__test__/drop/zips/single-file.zip"); console.log(`Output Path: ${outputPath}`); - const zipBuffer = zip.toBuffer(); + const zipBuffer = zip.toBuffer() as Buffer; const file = new File([zipBuffer], "single.zip"); await unzipDrop(file, baseApp); const files = await fs.readdir(outputPath, { withFileTypes: true }); diff --git a/apps/dokploy/__test__/env/environment.test.ts b/apps/dokploy/__test__/env/environment.test.ts new file mode 100644 index 0000000000..95d46dcc03 --- /dev/null +++ b/apps/dokploy/__test__/env/environment.test.ts @@ -0,0 +1,335 @@ +import { prepareEnvironmentVariables } from "@dokploy/server/index"; +import { describe, expect, it } from "vitest"; + +const projectEnv = ` +ENVIRONMENT=staging +DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db +PORT=3000 +`; + +const environmentEnv = ` +NODE_ENV=development +API_URL=https://api.dev.example.com +REDIS_URL=redis://localhost:6379 +DATABASE_NAME=dev_database +SECRET_KEY=env-secret-123 +`; + +describe("prepareEnvironmentVariables (environment variables)", () => { + it("resolves environment variables correctly", () => { + const serviceWithEnvVars = ` +NODE_ENV=\${{environment.NODE_ENV}} +API_URL=\${{environment.API_URL}} +SERVICE_PORT=4000 +`; + + const resolved = prepareEnvironmentVariables( + serviceWithEnvVars, + "", + environmentEnv, + ); + + expect(resolved).toEqual([ + "NODE_ENV=development", + "API_URL=https://api.dev.example.com", + "SERVICE_PORT=4000", + ]); + }); + + it("resolves both project and environment variables", () => { + const serviceWithBoth = ` +ENVIRONMENT=\${{project.ENVIRONMENT}} +NODE_ENV=\${{environment.NODE_ENV}} +API_URL=\${{environment.API_URL}} +DATABASE_URL=\${{project.DATABASE_URL}} +SERVICE_PORT=4000 +`; + + const resolved = prepareEnvironmentVariables( + serviceWithBoth, + projectEnv, + environmentEnv, + ); + + expect(resolved).toEqual([ + "ENVIRONMENT=staging", + "NODE_ENV=development", + "API_URL=https://api.dev.example.com", + "DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db", + "SERVICE_PORT=4000", + ]); + }); + + it("handles undefined environment variables", () => { + const serviceWithUndefined = ` +UNDEFINED_VAR=\${{environment.UNDEFINED_VAR}} +`; + + expect(() => + prepareEnvironmentVariables(serviceWithUndefined, "", environmentEnv), + ).toThrow("Invalid environment variable: environment.UNDEFINED_VAR"); + }); + + it("allows service variables to override environment variables", () => { + const serviceOverrideEnv = ` +NODE_ENV=production +API_URL=\${{environment.API_URL}} +`; + + const resolved = prepareEnvironmentVariables( + serviceOverrideEnv, + "", + environmentEnv, + ); + + expect(resolved).toEqual([ + "NODE_ENV=production", // Overrides environment variable + "API_URL=https://api.dev.example.com", + ]); + }); + + it("resolves complex references with project, environment, and service variables", () => { + const complexServiceEnv = ` +FULL_DATABASE_URL=\${{project.DATABASE_URL}}/\${{environment.DATABASE_NAME}} +API_ENDPOINT=\${{environment.API_URL}}/\${{project.ENVIRONMENT}}/api +SERVICE_NAME=my-service +COMPLEX_VAR=\${{SERVICE_NAME}}-\${{environment.NODE_ENV}}-\${{project.ENVIRONMENT}} +`; + + const resolved = prepareEnvironmentVariables( + complexServiceEnv, + projectEnv, + environmentEnv, + ); + + expect(resolved).toEqual([ + "FULL_DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db/dev_database", + "API_ENDPOINT=https://api.dev.example.com/staging/api", + "SERVICE_NAME=my-service", + "COMPLEX_VAR=my-service-development-staging", + ]); + }); + + it("handles environment variables with special characters", () => { + const specialEnvVars = ` +SPECIAL_URL=https://special.com +COMPLEX_KEY="key-with-@#$%^&*()" +JWT_SECRET="secret-with-spaces and symbols!@#" +`; + + const serviceWithSpecial = ` +FULL_URL=\${{environment.SPECIAL_URL}}/path?key=\${{environment.COMPLEX_KEY}} +AUTH_SECRET=\${{environment.JWT_SECRET}} +`; + + const resolved = prepareEnvironmentVariables( + serviceWithSpecial, + "", + specialEnvVars, + ); + + expect(resolved).toEqual([ + "FULL_URL=https://special.com/path?key=key-with-@#$%^&*()", + "AUTH_SECRET=secret-with-spaces and symbols!@#", + ]); + }); + + it("maintains precedence: service > environment > project", () => { + const conflictingProjectEnv = ` +NODE_ENV=production-project +API_URL=https://project.api.com +DATABASE_NAME=project_db +`; + + const conflictingEnvironmentEnv = ` +NODE_ENV=development-environment +API_URL=https://environment.api.com +DATABASE_NAME=env_db +`; + + const serviceWithConflicts = ` +NODE_ENV=service-override +PROJECT_ENV=\${{project.NODE_ENV}} +ENV_VAR=\${{environment.API_URL}} +DB_NAME=\${{environment.DATABASE_NAME}} +`; + + const resolved = prepareEnvironmentVariables( + serviceWithConflicts, + conflictingProjectEnv, + conflictingEnvironmentEnv, + ); + + expect(resolved).toEqual([ + "NODE_ENV=service-override", // Service wins + "PROJECT_ENV=production-project", // Project reference + "ENV_VAR=https://environment.api.com", // Environment reference + "DB_NAME=env_db", // Environment reference + ]); + }); + + it("handles empty environment variables", () => { + const serviceWithEmpty = ` +SERVICE_VAR=test +PROJECT_VAR=\${{project.ENVIRONMENT}} +`; + + const resolved = prepareEnvironmentVariables( + serviceWithEmpty, + projectEnv, + "", + ); + + expect(resolved).toEqual(["SERVICE_VAR=test", "PROJECT_VAR=staging"]); + }); + + it("handles mixed quotes and environment variables", () => { + const envWithQuotes = ` +QUOTED_VAR="development" +SINGLE_QUOTED='https://api.dev.example.com' +MIXED_VAR="value with 'single' quotes" +`; + + const serviceWithQuotes = ` +NODE_ENV=\${{environment.QUOTED_VAR}} +API_URL=\${{environment.SINGLE_QUOTED}} +COMPLEX="Prefix-\${{environment.MIXED_VAR}}-Suffix" +`; + + const resolved = prepareEnvironmentVariables( + serviceWithQuotes, + "", + envWithQuotes, + ); + + expect(resolved).toEqual([ + "NODE_ENV=development", + "API_URL=https://api.dev.example.com", + "COMPLEX=Prefix-value with 'single' quotes-Suffix", + ]); + }); + + it("resolves multiple environment references in single value", () => { + const multiRefEnv = ` +HOST=localhost +PORT=5432 +USERNAME=postgres +PASSWORD=secret123 +`; + + const serviceWithMultiRefs = ` +DATABASE_URL=postgresql://\${{environment.USERNAME}}:\${{environment.PASSWORD}}@\${{environment.HOST}}:\${{environment.PORT}}/mydb +CONNECTION_STRING=\${{environment.HOST}}:\${{environment.PORT}} +`; + + const resolved = prepareEnvironmentVariables( + serviceWithMultiRefs, + "", + multiRefEnv, + ); + + expect(resolved).toEqual([ + "DATABASE_URL=postgresql://postgres:secret123@localhost:5432/mydb", + "CONNECTION_STRING=localhost:5432", + ]); + }); + + it("handles nested references with environment and project variables", () => { + const nestedProjectEnv = ` +BASE_DOMAIN=example.com +PROTOCOL=https +`; + + const nestedEnvironmentEnv = ` +SUBDOMAIN=api.dev +PATH_PREFIX=/v1 +`; + + const serviceWithNested = ` +FULL_URL=\${{project.PROTOCOL}}://\${{environment.SUBDOMAIN}}.\${{project.BASE_DOMAIN}}\${{environment.PATH_PREFIX}}/endpoint +API_BASE=\${{project.PROTOCOL}}://\${{environment.SUBDOMAIN}}.\${{project.BASE_DOMAIN}} +`; + + const resolved = prepareEnvironmentVariables( + serviceWithNested, + nestedProjectEnv, + nestedEnvironmentEnv, + ); + + expect(resolved).toEqual([ + "FULL_URL=https://api.dev.example.com/v1/endpoint", + "API_BASE=https://api.dev.example.com", + ]); + }); + + it("throws error for malformed environment variable references", () => { + const serviceWithMalformed = ` +MALFORMED1=\${{environment.}} +MALFORMED2=\${{environment}} +VALID=\${{environment.NODE_ENV}} +`; + + // Should throw error for empty variable name after environment. + expect(() => + prepareEnvironmentVariables(serviceWithMalformed, "", environmentEnv), + ).toThrow("Invalid environment variable: environment."); + }); + + it("handles environment variables with numeric values", () => { + const numericEnv = ` +PORT=8080 +TIMEOUT=30 +RETRY_COUNT=3 +PERCENTAGE=99.5 +`; + + const serviceWithNumeric = ` +SERVER_PORT=\${{environment.PORT}} +REQUEST_TIMEOUT=\${{environment.TIMEOUT}} +MAX_RETRIES=\${{environment.RETRY_COUNT}} +SUCCESS_RATE=\${{environment.PERCENTAGE}} +`; + + const resolved = prepareEnvironmentVariables( + serviceWithNumeric, + "", + numericEnv, + ); + + expect(resolved).toEqual([ + "SERVER_PORT=8080", + "REQUEST_TIMEOUT=30", + "MAX_RETRIES=3", + "SUCCESS_RATE=99.5", + ]); + }); + + it("handles boolean-like environment variables", () => { + const booleanEnv = ` +DEBUG=true +ENABLED=false +PRODUCTION=1 +DEVELOPMENT=0 +`; + + const serviceWithBoolean = ` +DEBUG_MODE=\${{environment.DEBUG}} +FEATURE_ENABLED=\${{environment.ENABLED}} +IS_PROD=\${{environment.PRODUCTION}} +IS_DEV=\${{environment.DEVELOPMENT}} +`; + + const resolved = prepareEnvironmentVariables( + serviceWithBoolean, + "", + booleanEnv, + ); + + expect(resolved).toEqual([ + "DEBUG_MODE=true", + "FEATURE_ENABLED=false", + "IS_PROD=1", + "IS_DEV=0", + ]); + }); +}); diff --git a/apps/dokploy/__test__/env/shared.test.ts b/apps/dokploy/__test__/env/shared.test.ts index 4a8448aa9d..5e231a5ccf 100644 --- a/apps/dokploy/__test__/env/shared.test.ts +++ b/apps/dokploy/__test__/env/shared.test.ts @@ -177,3 +177,77 @@ COMPLEX_VAR="'Prefix \"DoubleQuoted\" and \${{project.APP_NAME}}'" ]); }); }); + +describe("prepareEnvironmentVariables (self references)", () => { + it("resolves self references correctly", () => { + const serviceEnv = ` +ENVIRONMENT=staging +DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db +SELF_REF=\${{ENVIRONMENT}} +`; + + const resolved = prepareEnvironmentVariables(serviceEnv, ""); + + expect(resolved).toEqual([ + "ENVIRONMENT=staging", + "DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db", + "SELF_REF=staging", + ]); + }); + + it("throws on undefined self references", () => { + const serviceEnv = ` +MISSING_VAR=\${{UNDEFINED_VAR}} +`; + + expect(() => prepareEnvironmentVariables(serviceEnv, "")).toThrow( + "Invalid service environment variable: UNDEFINED_VAR", + ); + }); + + it("allows overriding and still resolving from self", () => { + const serviceEnv = ` +ENVIRONMENT=production +OVERRIDE_ENV=\${{ENVIRONMENT}} +`; + + const resolved = prepareEnvironmentVariables(serviceEnv, ""); + + expect(resolved).toEqual([ + "ENVIRONMENT=production", + "OVERRIDE_ENV=production", + ]); + }); + + it("resolves multiple self references inside one value", () => { + const serviceEnv = ` +ENVIRONMENT=staging +APP_NAME=MyApp +COMPLEX=\${{APP_NAME}}-\${{ENVIRONMENT}}-\${{APP_NAME}} +`; + + const resolved = prepareEnvironmentVariables(serviceEnv, ""); + + expect(resolved).toEqual([ + "ENVIRONMENT=staging", + "APP_NAME=MyApp", + "COMPLEX=MyApp-staging-MyApp", + ]); + }); + + it("handles quotes with self references", () => { + const serviceEnv = ` +ENVIRONMENT=production +QUOTED="'\${{ENVIRONMENT}}'" +MIXED="\"Double \${{ENVIRONMENT}}\"" +`; + + const resolved = prepareEnvironmentVariables(serviceEnv, ""); + + expect(resolved).toEqual([ + "ENVIRONMENT=production", + "QUOTED='production'", + 'MIXED="Double production"', + ]); + }); +}); diff --git a/apps/dokploy/__test__/requests/request.test.ts b/apps/dokploy/__test__/requests/request.test.ts index 997bd9ec5f..53ca8d7772 100644 --- a/apps/dokploy/__test__/requests/request.test.ts +++ b/apps/dokploy/__test__/requests/request.test.ts @@ -1,5 +1,6 @@ import { parseRawConfig, processLogs } from "@dokploy/server"; import { describe, expect, it } from "vitest"; + const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`; describe("processLogs", () => { diff --git a/apps/dokploy/__test__/server/schema-trimming.test.ts b/apps/dokploy/__test__/server/schema-trimming.test.ts new file mode 100644 index 0000000000..5a3ee24c73 --- /dev/null +++ b/apps/dokploy/__test__/server/schema-trimming.test.ts @@ -0,0 +1,69 @@ +import { describe, it, expect } from 'vitest'; +import { apiCreateServer, apiUpdateServer } from '@dokploy/server/db/schema/server'; + +describe('Server Schema IP Address Trimming', () => { + it('should trim whitespace from IP address in apiCreateServer', () => { + const testData = { + name: 'Test Server', + description: 'Testing schema trimming', + ipAddress: ' 192.168.1.100 ', // IP with leading and trailing spaces + port: 22, + username: 'root', + sshKeyId: 'test-ssh-key-id' + }; + + const result = apiCreateServer.parse(testData); + + expect(result.ipAddress).toBe('192.168.1.100'); + expect(result.name).toBe('Test Server'); + expect(result.port).toBe(22); + }); + + it('should trim whitespace from IP address in apiUpdateServer', () => { + const testData = { + name: 'Test Server', + description: 'Testing schema trimming', + serverId: 'test-server-id', + ipAddress: ' 192.168.1.100 ', // IP with leading and trailing spaces + port: 22, + username: 'root', + sshKeyId: 'test-ssh-key-id' + }; + + const result = apiUpdateServer.parse(testData); + + expect(result.ipAddress).toBe('192.168.1.100'); + expect(result.serverId).toBe('test-server-id'); + expect(result.name).toBe('Test Server'); + }); + + it('should handle empty string IP address', () => { + const testData = { + name: 'Test Server', + description: 'Testing schema trimming', + ipAddress: '', // Empty string + port: 22, + username: 'root', + sshKeyId: 'test-ssh-key-id' + }; + + const result = apiCreateServer.parse(testData); + + expect(result.ipAddress).toBe(''); + }); + + it('should handle IP address with only spaces', () => { + const testData = { + name: 'Test Server', + description: 'Testing schema trimming', + ipAddress: ' ', // Only spaces + port: 22, + username: 'root', + sshKeyId: 'test-ssh-key-id' + }; + + const result = apiCreateServer.parse(testData); + + expect(result.ipAddress).toBe(''); + }); +}); diff --git a/apps/dokploy/__test__/traefik/traefik.test.ts b/apps/dokploy/__test__/traefik/traefik.test.ts index 5cd033af56..5be96e4733 100644 --- a/apps/dokploy/__test__/traefik/traefik.test.ts +++ b/apps/dokploy/__test__/traefik/traefik.test.ts @@ -1,12 +1,12 @@ -import type { Domain } from "@dokploy/server"; -import type { Redirect } from "@dokploy/server"; -import type { ApplicationNested } from "@dokploy/server"; +import type { ApplicationNested, Domain, Redirect } from "@dokploy/server"; import { createRouterConfig } from "@dokploy/server"; import { expect, test } from "vitest"; const baseApp: ApplicationNested = { + railpackVersion: "0.2.2", rollbackActive: false, applicationId: "", + previewLabels: [], herokuVersion: "", giteaRepository: "", giteaOwner: "", @@ -18,6 +18,7 @@ const baseApp: ApplicationNested = { appName: "", autoDeploy: true, enableSubmodules: false, + previewRequireCollaboratorPermissions: false, serverId: "", branch: null, dockerBuildStage: "", @@ -35,13 +36,22 @@ const baseApp: ApplicationNested = { previewLimit: 0, previewCustomCertResolver: null, previewWildcard: "", - project: { + environmentId: "", + environment: { env: "", - organizationId: "", + environmentId: "", name: "", - description: "", createdAt: "", + description: "", projectId: "", + project: { + env: "", + organizationId: "", + name: "", + description: "", + createdAt: "", + projectId: "", + }, }, buildPath: "/", gitlabPathNamespace: "", @@ -84,7 +94,6 @@ const baseApp: ApplicationNested = { password: null, placementSwarm: null, ports: [], - projectId: "", publishDirectory: null, isStaticSpa: null, redirects: [], @@ -119,6 +128,8 @@ const baseDomain: Domain = { domainType: "application", uniqueConfigKey: 1, previewDeploymentId: "", + internalPath: "/", + stripPath: false, }; const baseRedirect: Redirect = { 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 95a559f662..9e10f43ec4 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 @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { HelpCircle, Settings } from "lucide-react"; +import { useEffect } 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"; @@ -26,12 +32,6 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { HelpCircle, Settings } from "lucide-react"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const HealthCheckSwarmSchema = z .object({ @@ -130,7 +130,7 @@ const createStringToJSONSchema = (schema: z.ZodTypeAny) => { } try { return JSON.parse(str); - } catch (_e) { + } catch { ctx.addIssue({ code: "custom", message: "Invalid JSON format" }); return z.NEVER; } @@ -181,21 +181,38 @@ const addSwarmSettings = z.object({ type AddSwarmSettings = z.infer; interface Props { - applicationId: string; + id: string; + type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; } -export const AddSwarmSettings = ({ applicationId }: Props) => { - const { data, refetch } = api.application.one.useQuery( - { - applicationId, - }, - { - enabled: !!applicationId, - }, - ); +export const AddSwarmSettings = ({ id, type }: Props) => { + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; - const { mutateAsync, isError, error, isLoading } = - api.application.update.useMutation(); + const { mutateAsync, isError, error, isLoading } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); const form = useForm({ defaultValues: { @@ -244,7 +261,12 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { const onSubmit = async (data: AddSwarmSettings) => { await mutateAsync({ - applicationId, + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", healthCheckSwarm: data.healthCheckSwarm, restartPolicySwarm: data.restartPolicySwarm, placementSwarm: data.placementSwarm, @@ -270,18 +292,18 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { Swarm Settings - - + + Swarm Settings Update certain settings using a json object. {isError && {error?.message}} -
+
- Changing settings such as placements may cause the logs/monitoring - to be unavailable. + Changing settings such as placements may cause the logs/monitoring, + backups and other features to be unavailable.
@@ -289,13 +311,13 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
( - + Health Check @@ -351,7 +373,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { control={form.control} name="restartPolicySwarm" render={({ field }) => ( - + Restart Policy @@ -405,7 +427,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { control={form.control} name="placementSwarm" render={({ field }) => ( - + Placement @@ -471,7 +493,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { control={form.control} name="updateConfigSwarm" render={({ field }) => ( - + Update Config @@ -529,7 +551,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { control={form.control} name="rollbackConfigSwarm" render={({ field }) => ( - + Rollback Config @@ -587,7 +609,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { control={form.control} name="modeSwarm" render={({ field }) => ( - + Mode @@ -650,7 +672,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { control={form.control} name="networkSwarm" render={({ field }) => ( - + Network @@ -709,7 +731,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { control={form.control} name="labelsSwarm" render={({ field }) => ( - + Labels @@ -753,7 +775,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { )} /> - +
- {registries && registries?.length === 0 ? ( -
-
- - - To use a cluster feature, you need to configure at least a - registry first. Please, go to{" "} - - Settings - {" "} - to do so. - -
-
- ) : ( + {type === "application" && ( <> - ( - - Select a registry - - - )} - /> + {registries && registries?.length === 0 ? ( +
+
+ + + To use a cluster feature, you need to configure at least + a registry first. Please, go to{" "} + + Settings + {" "} + to do so. + +
+
+ ) : ( + <> + ( + + Select a registry + + + )} + /> + + )} )} diff --git a/apps/dokploy/components/dashboard/application/advanced/general/add-command.tsx b/apps/dokploy/components/dashboard/application/advanced/general/add-command.tsx index 50e36ad760..1bf69394ad 100644 --- a/apps/dokploy/components/dashboard/application/advanced/general/add-command.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/general/add-command.tsx @@ -1,3 +1,8 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; import { Button } from "@/components/ui/button"; import { Card, @@ -16,11 +21,7 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; + interface Props { applicationId: string; } diff --git a/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx b/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx index aa359d67bf..17d033cf27 100644 --- a/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { Code2, Globe2, HardDrive } from "lucide-react"; +import { useEffect, 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"; @@ -27,12 +33,6 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Code2, Globe2, HardDrive } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const ImportSchema = z.object({ base64: z.string(), @@ -107,7 +107,7 @@ export const ShowImport = ({ composeId }: Props) => { composeId, }); setShowModal(false); - } catch (_error) { + } catch { toast.error("Error importing template"); } }; @@ -126,7 +126,7 @@ export const ShowImport = ({ composeId }: Props) => { }); setTemplateInfo(result); setShowModal(true); - } catch (_error) { + } catch { toast.error("Error processing template"); } }; @@ -185,7 +185,7 @@ export const ShowImport = ({ composeId }: Props) => {
- + Template Information diff --git a/apps/dokploy/components/dashboard/application/advanced/ports/handle-ports.tsx b/apps/dokploy/components/dashboard/application/advanced/ports/handle-ports.tsx index c9758e37f6..568792461b 100644 --- a/apps/dokploy/components/dashboard/application/advanced/ports/handle-ports.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/ports/handle-ports.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { PenBoxIcon, PlusIcon } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useForm, useWatch } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; import { @@ -26,15 +32,12 @@ import { SelectValue, } from "@/components/ui/select"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { PenBoxIcon, PlusIcon } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const AddPortSchema = z.object({ publishedPort: z.number().int().min(1).max(65535), + publishMode: z.enum(["ingress", "host"], { + required_error: "Publish mode is required", + }), targetPort: z.number().int().min(1).max(65535), protocol: z.enum(["tcp", "udp"], { required_error: "Protocol is required", @@ -77,9 +80,15 @@ export const HandlePorts = ({ resolver: zodResolver(AddPortSchema), }); + const publishMode = useWatch({ + control: form.control, + name: "publishMode", + }); + useEffect(() => { form.reset({ publishedPort: data?.publishedPort ?? 0, + publishMode: data?.publishMode ?? "ingress", targetPort: data?.targetPort ?? 0, protocol: data?.protocol ?? "tcp", }); @@ -120,7 +129,7 @@ export const HandlePorts = ({ )} - + Ports @@ -165,6 +174,32 @@ export const HandlePorts = ({ )} /> + { + return ( + + Published Port Mode + + + + ); + }} + /> + {publishMode === "host" && ( + + Host Mode Limitation: When using Host publish + mode, Docker Swarm has limitations that prevent proper container + updates during deployments. Old containers may not be replaced + automatically. Consider using Ingress mode instead, or be prepared + to manually stop/start the application after deployments. + + )} + )} - + Redirects diff --git a/apps/dokploy/components/dashboard/application/advanced/redirects/show-redirects.tsx b/apps/dokploy/components/dashboard/application/advanced/redirects/show-redirects.tsx index 5c2c5943c5..f1b14bfc01 100644 --- a/apps/dokploy/components/dashboard/application/advanced/redirects/show-redirects.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/redirects/show-redirects.tsx @@ -1,3 +1,5 @@ +import { Split, Trash2 } from "lucide-react"; +import { toast } from "sonner"; import { DialogAction } from "@/components/shared/dialog-action"; import { Button } from "@/components/ui/button"; import { @@ -8,8 +10,6 @@ import { CardTitle, } from "@/components/ui/card"; import { api } from "@/utils/api"; -import { Split, Trash2 } from "lucide-react"; -import { toast } from "sonner"; import { HandleRedirect } from "./handle-redirect"; interface Props { diff --git a/apps/dokploy/components/dashboard/application/advanced/security/handle-security.tsx b/apps/dokploy/components/dashboard/application/advanced/security/handle-security.tsx index e7bc0cd1f3..c52976eb12 100644 --- a/apps/dokploy/components/dashboard/application/advanced/security/handle-security.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/security/handle-security.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { PenBoxIcon, PlusIcon } from "lucide-react"; +import { useEffect, 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 { Button } from "@/components/ui/button"; import { @@ -19,12 +25,6 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { PenBoxIcon, PlusIcon } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const AddSecuritychema = z.object({ username: z.string().min(1, "Username is required"), @@ -114,7 +114,7 @@ export const HandleSecurity = ({ )} - + Security @@ -151,7 +151,7 @@ export const HandleSecurity = ({ Password - + diff --git a/apps/dokploy/components/dashboard/application/advanced/security/show-security.tsx b/apps/dokploy/components/dashboard/application/advanced/security/show-security.tsx index 92439f5117..5676e6f002 100644 --- a/apps/dokploy/components/dashboard/application/advanced/security/show-security.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/security/show-security.tsx @@ -1,4 +1,7 @@ +import { LockKeyhole, Trash2 } from "lucide-react"; +import { toast } from "sonner"; import { DialogAction } from "@/components/shared/dialog-action"; +import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { Button } from "@/components/ui/button"; import { Card, @@ -7,9 +10,9 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; -import { LockKeyhole, Trash2 } from "lucide-react"; -import { toast } from "sonner"; import { HandleSecurity } from "./handle-security"; interface Props { @@ -58,19 +61,18 @@ export const ShowSecurity = ({ applicationId }: Props) => {
{data?.security.map((security) => (
-
-
-
- Username - - {security.username} - +
+
+
+ +
-
- Password - - {security.password} - +
+ +
diff --git a/apps/dokploy/components/dashboard/application/advanced/show-resources.tsx b/apps/dokploy/components/dashboard/application/advanced/show-resources.tsx index 3d26716fcf..25040067b4 100644 --- a/apps/dokploy/components/dashboard/application/advanced/show-resources.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/show-resources.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { InfoIcon } from "lucide-react"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; import { @@ -23,12 +29,6 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { InfoIcon } from "lucide-react"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const addResourcesSchema = z.object({ memoryReservation: z.string().optional(), diff --git a/apps/dokploy/components/dashboard/application/advanced/traefik/show-traefik-config.tsx b/apps/dokploy/components/dashboard/application/advanced/traefik/show-traefik-config.tsx index 58601fb494..ae23f18668 100644 --- a/apps/dokploy/components/dashboard/application/advanced/traefik/show-traefik-config.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/traefik/show-traefik-config.tsx @@ -1,3 +1,4 @@ +import { File, Loader2 } from "lucide-react"; import { CodeEditor } from "@/components/shared/code-editor"; import { Card, @@ -7,8 +8,8 @@ import { CardTitle, } from "@/components/ui/card"; import { api } from "@/utils/api"; -import { File, Loader2 } from "lucide-react"; import { UpdateTraefikConfig } from "./update-traefik-config"; + interface Props { applicationId: string; } diff --git a/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx b/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx index f563f1ab49..bf3d5d9bc9 100644 --- a/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { parse, stringify, YAMLParseError } from "yaml"; +import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; import { CodeEditor } from "@/components/shared/code-editor"; import { Button } from "@/components/ui/button"; @@ -19,12 +25,6 @@ import { FormMessage, } from "@/components/ui/form"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import jsyaml from "js-yaml"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const UpdateTraefikConfigSchema = z.object({ traefikConfig: z.string(), @@ -38,11 +38,11 @@ interface Props { export const validateAndFormatYAML = (yamlText: string) => { try { - const obj = jsyaml.load(yamlText); - const formattedYaml = jsyaml.dump(obj, { indent: 4 }); + const obj = parse(yamlText); + const formattedYaml = stringify(obj, { indent: 4 }); return { valid: true, formattedYaml, error: null }; } catch (error) { - if (error instanceof jsyaml.YAMLException) { + if (error instanceof YAMLParseError) { return { valid: false, formattedYaml: yamlText, @@ -89,7 +89,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => { if (!valid) { form.setError("traefikConfig", { type: "manual", - message: error || "Invalid YAML", + message: (error as string) || "Invalid YAML", }); return; } @@ -122,7 +122,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => { - + Update traefik config Update the traefik config diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx index 718f98b726..00be8a1e1b 100644 --- a/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx @@ -1,3 +1,11 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { PlusIcon } from "lucide-react"; +import type React from "react"; +import { useEffect, 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 { @@ -21,13 +29,7 @@ import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { PlusIcon } from "lucide-react"; -import type React from "react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; + interface Props { serviceId: string; serviceType: @@ -150,7 +152,7 @@ export const AddVolumes = ({ - + Volumes / Mounts @@ -169,6 +171,23 @@ export const AddVolumes = ({ onSubmit={form.handleSubmit(onSubmit)} className="grid w-full gap-8 " > + {type === "bind" && ( + +
+

+ Make sure the host path is a valid path and exists in the + host machine. +

+

+ Cluster Warning: If you're using cluster + features, bind mounts may cause deployment failures since + the path must exist on all worker/manager nodes. Consider + using external tools to distribute the folder across nodes + or use named volumes instead. +

+
+
+ )} { className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4" > {/* */} -
+
Mount Type @@ -112,21 +113,21 @@ export const ShowVolumes = ({ id, type }: Props) => {
)} - {mount.type === "file" ? ( + {mount.type === "file" && (
File Path {mount.filePath}
- ) : ( -
- Mount Path - - {mount.mountPath} - -
)} + +
+ Mount Path + + {mount.mountPath} + +
- + Update Update the mount diff --git a/apps/dokploy/components/dashboard/application/build/show.tsx b/apps/dokploy/components/dashboard/application/build/show.tsx index 291026d4f8..1a0ed386d0 100644 --- a/apps/dokploy/components/dashboard/application/build/show.tsx +++ b/apps/dokploy/components/dashboard/application/build/show.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { Cog } from "lucide-react"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -15,12 +21,6 @@ import { import { Input } from "@/components/ui/input"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Cog } from "lucide-react"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; export enum BuildType { dockerfile = "dockerfile", @@ -65,6 +65,7 @@ const mySchema = z.discriminatedUnion("buildType", [ }), z.object({ buildType: z.literal(BuildType.railpack), + railpackVersion: z.string().nullable().default("0.2.2"), }), z.object({ buildType: z.literal(BuildType.static), @@ -86,6 +87,7 @@ interface ApplicationData { herokuVersion?: string | null; publishDirectory?: string | null; isStaticSpa?: boolean | null; + railpackVersion?: string | null | undefined; } function isValidBuildType(value: string): value is BuildType { @@ -123,6 +125,7 @@ const resetData = (data: ApplicationData): AddTemplate => { case BuildType.railpack: return { buildType: BuildType.railpack, + railpackVersion: data.railpackVersion || null, }; default: { const buildType = data.buildType as BuildType; @@ -181,6 +184,10 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { : null, isStaticSpa: data.buildType === BuildType.static ? data.isStaticSpa : null, + railpackVersion: + data.buildType === BuildType.railpack + ? data.railpackVersion || "0.2.2" + : null, }) .then(async () => { toast.success("Build type saved"); @@ -395,6 +402,25 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { )} /> )} + {buildType === BuildType.railpack && ( + ( + + Railpack Version + + + + + + )} + /> + )}
)} - + { + if (!isCloud || !deployments || deployments.length === 0) return null; + + const now = Date.now(); + const NINE_MINUTES = 10 * 60 * 1000; // 9 minutes in milliseconds + + // Get the most recent deployment (first in the list since they're sorted by date) + const mostRecentDeployment = deployments[0]; + + if ( + !mostRecentDeployment || + mostRecentDeployment.status !== "running" || + !mostRecentDeployment.startedAt + ) { + return null; + } + + const startTime = new Date(mostRecentDeployment.startedAt).getTime(); + const elapsed = now - startTime; + + return elapsed > NINE_MINUTES ? mostRecentDeployment : null; + }, [isCloud, deployments]); useEffect(() => { setUrl(document.location.origin); }, []); @@ -74,7 +114,7 @@ export const ShowDeployments = ({
Deployments - See all the 10 last deployments for this {type} + See the last 10 deployments for this {type}
@@ -91,6 +131,54 @@ export const ShowDeployments = ({
+ {stuckDeployment && (type === "application" || type === "compose") && ( + +
+
+
+ Build appears to be stuck +
+

+ Hey! Looks like the build has been running for more than 10 + minutes. Would you like to cancel this deployment? +

+
+ +
+
+ )} {refreshToken && (
@@ -101,7 +189,9 @@ export const ShowDeployments = ({ Webhook URL:
- {`${url}/api/deploy${type === "compose" ? "/compose" : ""}/${refreshToken}`} + {`${url}/api/deploy${ + type === "compose" ? "/compose" : "" + }/${refreshToken}`} {(type === "application" || type === "compose") && ( @@ -170,6 +260,32 @@ export const ShowDeployments = ({
+ {deployment.pid && deployment.status === "running" && ( + { + await killProcess({ + deploymentId: deployment.deploymentId, + }) + .then(() => { + toast.success("Process killed successfully"); + }) + .catch(() => { + toast.error("Error killing process"); + }); + }} + > + + + )} - + diff --git a/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx b/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx index c145afcfcf..9d7a074f98 100644 --- a/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx +++ b/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx @@ -1,3 +1,10 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { DatabaseZap, Dices, RefreshCw } from "lucide-react"; +import Link from "next/link"; +import { useEffect, 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 { Button } from "@/components/ui/button"; import { @@ -34,14 +41,6 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { api } from "@/utils/api"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { DatabaseZap, Dices, RefreshCw } from "lucide-react"; -import Link from "next/link"; -import z from "zod"; export type CacheType = "fetch" | "cache"; @@ -49,6 +48,8 @@ export const domain = z .object({ host: z.string().min(1, { message: "Add a hostname" }), path: z.string().min(1).optional(), + internalPath: z.string().optional(), + stripPath: z.boolean().optional(), port: z .number() .min(1, { message: "Port must be at least 1" }) @@ -84,6 +85,29 @@ export const domain = z message: "Required", }); } + + // Validate stripPath requires a valid path + if (input.stripPath && (!input.path || input.path === "/")) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["stripPath"], + message: + "Strip path can only be enabled when a path other than '/' is specified", + }); + } + + // Validate internalPath starts with / + if ( + input.internalPath && + input.internalPath !== "/" && + !input.internalPath.startsWith("/") + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["internalPath"], + message: "Internal path must start with '/'", + }); + } }); type Domain = z.infer; @@ -98,6 +122,7 @@ interface Props { export const AddDomain = ({ id, type, domainId = "", children }: Props) => { const [isOpen, setIsOpen] = useState(false); const [cacheType, setCacheType] = useState("cache"); + const [isManualInput, setIsManualInput] = useState(false); const utils = api.useUtils(); const { data, refetch } = api.domain.one.useQuery( @@ -162,6 +187,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => { defaultValues: { host: "", path: undefined, + internalPath: undefined, + stripPath: false, port: undefined, https: false, certificateType: undefined, @@ -182,6 +209,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => { ...data, /* Convert null to undefined */ path: data?.path || undefined, + internalPath: data?.internalPath || undefined, + stripPath: data?.stripPath || false, port: data?.port || undefined, certificateType: data?.certificateType || undefined, customCertResolver: data?.customCertResolver || undefined, @@ -194,6 +223,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => { form.reset({ host: "", path: undefined, + internalPath: undefined, + stripPath: false, port: undefined, https: false, certificateType: undefined, @@ -261,7 +292,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => { {children} - + Domain {dictionary.dialogDescription} @@ -294,76 +325,126 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => { Service Name
- - - - {services?.map((service, index) => ( - - {service} + ) : ( + - - - - - - -

- Fetch: Will clone the repository and load - the services -

-
-
-
+
+ + )} + {!isManualInput && ( + <> + + + + + + +

+ Fetch: Will clone the repository and + load the services +

+
+
+
+ + + + + + +

+ Cache: If you previously deployed this + compose, it will read the services + from the last deployment/fetch from + the repository +

+
+
+
+ + )} { className="max-w-[10rem]" >

- Cache: If you previously deployed this - compose, it will read the services from - the last deployment/fetch from the - repository + {isManualInput + ? "Switch to service selection" + : "Enter service name manually"}

@@ -469,6 +549,49 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => { }} /> + { + return ( + + Internal Path + + The path where your application expects to receive + requests internally (defaults to "/") + + + + + + + ); + }} + /> + + ( + +
+ Strip Path + + Remove the external path from the request before + forwarding to the application + + +
+ + + +
+ )} + /> + { const repository = form.watch("repository"); const gitlabId = form.watch("gitlabId"); + const gitlabUrl = useMemo(() => { + const url = gitlabProviders?.find( + (provider) => provider.gitlabId === gitlabId, + )?.gitlabUrl; + + const gitlabUrl = url?.replace(/\/$/, ""); + + return gitlabUrl || "https://gitlab.com"; + }, [gitlabId, gitlabProviders]); + const { data: repositories, isLoading: isLoadingRepositories, @@ -224,7 +234,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => { Repository {field.value.owner && field.value.repo && ( { {repositories?.map((repo) => { return ( { form.setValue("repository", { @@ -299,7 +309,8 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => { { setSab(e as TabState); }} > -
- +
+ { toast.success("Application deployed successfully"); refetch(); router.push( - `/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`, + `/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/application/${applicationId}?tab=deployments`, ); }) .catch(() => { diff --git a/apps/dokploy/components/dashboard/application/logs/show.tsx b/apps/dokploy/components/dashboard/application/logs/show.tsx index a73b99d253..e5dff075ee 100644 --- a/apps/dokploy/components/dashboard/application/logs/show.tsx +++ b/apps/dokploy/components/dashboard/application/logs/show.tsx @@ -1,3 +1,6 @@ +import { Loader2 } from "lucide-react"; +import dynamic from "next/dynamic"; +import { useEffect, useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Card, @@ -18,9 +21,6 @@ import { } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; -import { Loader2 } from "lucide-react"; -import dynamic from "next/dynamic"; -import { useEffect, useState } from "react"; export const DockerLogs = dynamic( () => import("@/components/dashboard/docker/logs/docker-logs-id").then( diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx index 78cd55d7a2..eac4559f13 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { Dices } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import type z from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; import { @@ -33,15 +39,8 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import { api } from "@/utils/api"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; - import { domain } from "@/server/db/validations/domain"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Dices } from "lucide-react"; -import type z from "zod"; +import { api } from "@/utils/api"; type Domain = z.infer; @@ -138,7 +137,7 @@ export const AddPreviewDomain = ({ {children} - + Domain {dictionary.dialogDescription} diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx index bf93af7189..d93bbd1c87 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx @@ -1,3 +1,13 @@ +import { + ExternalLink, + FileText, + GitPullRequest, + Loader2, + PenSquare, + RocketIcon, + Trash2, +} from "lucide-react"; +import { toast } from "sonner"; import { GithubIcon } from "@/components/icons/data-tools-icons"; import { DateTooltip } from "@/components/shared/date-tooltip"; import { DialogAction } from "@/components/shared/dialog-action"; @@ -13,16 +23,6 @@ import { } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; -import { - ExternalLink, - FileText, - GitPullRequest, - Loader2, - PenSquare, - RocketIcon, - Trash2, -} from "lucide-react"; -import { toast } from "sonner"; import { ShowModalLogs } from "../../settings/web-server/show-modal-logs"; import { ShowDeploymentsModal } from "../deployments/show-deployments-modal"; import { AddPreviewDomain } from "./add-preview-domain"; diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx index 4c5068eee9..16c916d934 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx @@ -1,3 +1,10 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { HelpCircle, Plus, Settings2, X } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -27,13 +34,13 @@ import { SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Settings2 } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const schema = z .object({ @@ -42,10 +49,12 @@ const schema = z wildcardDomain: z.string(), port: z.number(), previewLimit: z.number(), + previewLabels: z.array(z.string()).optional(), previewHttps: z.boolean(), previewPath: z.string(), previewCertificateType: z.enum(["letsencrypt", "none", "custom"]), previewCustomCertResolver: z.string().optional(), + previewRequireCollaboratorPermissions: z.boolean(), }) .superRefine((input, ctx) => { if ( @@ -80,9 +89,11 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => { wildcardDomain: "*.traefik.me", port: 3000, previewLimit: 3, + previewLabels: [], previewHttps: false, previewPath: "/", previewCertificateType: "none", + previewRequireCollaboratorPermissions: true, }, resolver: zodResolver(schema), }); @@ -100,11 +111,14 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => { buildArgs: data.previewBuildArgs || "", wildcardDomain: data.previewWildcard || "*.traefik.me", port: data.previewPort || 3000, + previewLabels: data.previewLabels || [], previewLimit: data.previewLimit || 3, previewHttps: data.previewHttps || false, previewPath: data.previewPath || "/", previewCertificateType: data.previewCertificateType || "none", previewCustomCertResolver: data.previewCustomCertResolver || "", + previewRequireCollaboratorPermissions: + data.previewRequireCollaboratorPermissions || true, }); } }, [data]); @@ -115,12 +129,15 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => { previewBuildArgs: formData.buildArgs, previewWildcard: formData.wildcardDomain, previewPort: formData.port, + previewLabels: formData.previewLabels, applicationId, previewLimit: formData.previewLimit, previewHttps: formData.previewHttps, previewPath: formData.previewPath, previewCertificateType: formData.previewCertificateType, previewCustomCertResolver: formData.previewCustomCertResolver, + previewRequireCollaboratorPermissions: + formData.previewRequireCollaboratorPermissions, }) .then(() => { toast.success("Preview Deployments settings updated"); @@ -138,7 +155,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => { Configure - + Preview Deployment Settings @@ -194,6 +211,90 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => { )} /> + ( + +
+ Preview Labels + + + + + + +

+ Add a labels that will trigger a preview + deployment for a pull request. If no labels + are specified, all pull requests will trigger + a preview deployment. +

+
+
+
+
+
+ {field.value?.map((label, index) => ( + + {label} + { + const newLabels = [...(field.value || [])]; + newLabels.splice(index, 1); + field.onChange(newLabels); + }} + /> + + ))} +
+
+ + { + if (e.key === "Enter") { + e.preventDefault(); + const input = e.currentTarget; + const label = input.value.trim(); + if (label) { + field.onChange([ + ...(field.value || []), + label, + ]); + input.value = ""; + } + } + }} + /> + + +
+ +
+ )} + /> {
+
+ ( + +
+ + Require Collaborator Permissions + + + Require collaborator permissions to preview + deployments, valid roles are: +
    +
  • Admin
  • +
  • Maintain
  • +
  • Write
  • +
+
+
+ + + +
+ )} + /> +
+ { Configure how rollbacks work for this application + + Having rollbacks enabled increases storage usage. Be careful with + this option. Note that manually cleaning the cache may delete + rollback images, making them unavailable for future rollbacks. +
diff --git a/apps/dokploy/components/dashboard/application/schedules/handle-schedules.tsx b/apps/dokploy/components/dashboard/application/schedules/handle-schedules.tsx index 2d26d7a94e..8273d0e2be 100644 --- a/apps/dokploy/components/dashboard/application/schedules/handle-schedules.tsx +++ b/apps/dokploy/components/dashboard/application/schedules/handle-schedules.tsx @@ -1,9 +1,22 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { + DatabaseZap, + Info, + PenBoxIcon, + PlusCircle, + RefreshCw, +} from "lucide-react"; +import { useEffect, useState } from "react"; +import { type Control, 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, @@ -34,18 +47,6 @@ import { } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { - DatabaseZap, - Info, - PenBoxIcon, - PlusCircle, - RefreshCw, -} from "lucide-react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; import type { CacheType } from "../domains/handle-domain"; export const commonCronExpressions = [ @@ -56,6 +57,7 @@ export const commonCronExpressions = [ { label: "Every month on the 1st at midnight", value: "0 0 1 * *" }, { label: "Every 15 minutes", value: "*/15 * * * *" }, { label: "Every weekday at midnight", value: "0 0 * * 1-5" }, + { label: "Custom", value: "custom" }, ]; const formSchema = z @@ -114,10 +116,91 @@ interface Props { scheduleType?: "application" | "compose" | "server" | "dokploy-server"; } +export const ScheduleFormField = ({ + name, + formControl, +}: { + name: string; + formControl: Control; +}) => { + const [selectedOption, setSelectedOption] = useState(""); + + return ( + ( + + + Schedule + + + + + + +

Cron expression format: minute hour day month weekday

+

Example: 0 0 * * * (daily at midnight)

+
+
+
+
+
+ +
+ + { + const value = e.target.value; + const commonExpression = commonCronExpressions.find( + (expression) => expression.value === value, + ); + if (commonExpression) { + setSelectedOption(commonExpression.value); + } else { + setSelectedOption("custom"); + } + field.onChange(e); + }} + /> + +
+
+ + Choose a predefined schedule or enter a custom cron expression + + +
+ )} + /> + ); +}; + export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => { const [isOpen, setIsOpen] = useState(false); const [cacheType, setCacheType] = useState("cache"); - const utils = api.useUtils(); const form = useForm>({ resolver: zodResolver(formSchema), @@ -232,14 +315,17 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => { {scheduleId ? "Edit" : "Create"} Schedule + + {scheduleId ? "Manage" : "Create"} a schedule to run a task at a + specific time or interval. + @@ -373,63 +459,9 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => { )} /> - ( - - - Schedule - - - - - - -

- Cron expression format: minute hour day month - weekday -

-

Example: 0 0 * * * (daily at midnight)

-
-
-
-
-
- -
- - - -
-
- - Choose a predefined schedule or enter a custom cron - expression - - -
- )} + formControl={form.control} /> {(scheduleTypeForm === "application" || diff --git a/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx b/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx index 672fb5ee31..3209b6e032 100644 --- a/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx +++ b/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx @@ -1,3 +1,12 @@ +import { + ClipboardList, + Clock, + Loader2, + Play, + Terminal, + Trash2, +} from "lucide-react"; +import { toast } from "sonner"; import { DialogAction } from "@/components/shared/dialog-action"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -15,15 +24,6 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { api } from "@/utils/api"; -import { - ClipboardList, - Clock, - Loader2, - Play, - Terminal, - Trash2, -} from "lucide-react"; -import { toast } from "sonner"; import { ShowDeploymentsModal } from "../deployments/show-deployments-modal"; import { HandleSchedules } from "./handle-schedules"; @@ -58,7 +58,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => { return ( -
+
Scheduled Tasks @@ -91,15 +91,15 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => { return (
-
+
-
-

+
+

{schedule.name}

{ {schedule.enabled ? "Enabled" : "Disabled"}
-
+
{
-
+
{ await runManually({ scheduleId: schedule.scheduleId, - }).then(async () => { - await new Promise((resolve) => - setTimeout(resolve, 1500), - ); - refetchSchedules(); - }); + }) + .then(async () => { + await new Promise((resolve) => + setTimeout(resolve, 1500), + ); + refetchSchedules(); + }) + .catch(() => { + toast.error("Error running schedule"); + }); }} > @@ -222,7 +226,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => { })}
) : ( -
+

No scheduled tasks diff --git a/apps/dokploy/components/dashboard/application/update-application.tsx b/apps/dokploy/components/dashboard/application/update-application.tsx index 934a596df0..754074d754 100644 --- a/apps/dokploy/components/dashboard/application/update-application.tsx +++ b/apps/dokploy/components/dashboard/application/update-application.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { PenBoxIcon } from "lucide-react"; +import { useEffect, 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 { Button } from "@/components/ui/button"; import { @@ -20,12 +26,6 @@ import { import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { PenBoxIcon } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const updateApplicationSchema = z.object({ name: z.string().min(1, { @@ -99,7 +99,7 @@ export const UpdateApplication = ({ applicationId }: Props) => { - + Modify Application Update the application data diff --git a/apps/dokploy/components/dashboard/application/volume-backups/handle-volume-backups.tsx b/apps/dokploy/components/dashboard/application/volume-backups/handle-volume-backups.tsx new file mode 100644 index 0000000000..804b4c39be --- /dev/null +++ b/apps/dokploy/components/dashboard/application/volume-backups/handle-volume-backups.tsx @@ -0,0 +1,635 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { DatabaseZap, PenBoxIcon, PlusCircle, RefreshCw } from "lucide-react"; +import { useEffect, 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 { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import { api } from "@/utils/api"; +import type { CacheType } from "../domains/handle-domain"; +import { ScheduleFormField } from "../schedules/handle-schedules"; + +const formSchema = z + .object({ + name: z.string().min(1, "Name is required"), + cronExpression: z.string().min(1, "Cron expression is required"), + volumeName: z.string().min(1, "Volume name is required"), + prefix: z.string(), + keepLatestCount: z.coerce + .number() + .int() + .gte(1, "Must be at least 1") + .optional() + .nullable(), + turnOff: z.boolean().default(false), + enabled: z.boolean().default(true), + serviceType: z.enum([ + "application", + "compose", + "postgres", + "mariadb", + "mongo", + "mysql", + "redis", + ]), + serviceName: z.string(), + destinationId: z.string().min(1, "Destination required"), + }) + .superRefine((data, ctx) => { + if (data.serviceType === "compose" && !data.serviceName) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Service name is required", + path: ["serviceName"], + }); + } + + if (data.serviceType === "compose" && !data.serviceName) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Service name is required", + path: ["serviceName"], + }); + } + }); + +interface Props { + id?: string; + volumeBackupId?: string; + volumeBackupType?: + | "application" + | "compose" + | "postgres" + | "mariadb" + | "mongo" + | "mysql" + | "redis"; +} + +export const HandleVolumeBackups = ({ + id, + volumeBackupId, + volumeBackupType, +}: Props) => { + const [isOpen, setIsOpen] = useState(false); + const [cacheType, setCacheType] = useState("cache"); + const [keepLatestCountInput, setKeepLatestCountInput] = useState(""); + + const utils = api.useUtils(); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + cronExpression: "", + volumeName: "", + prefix: "", + keepLatestCount: undefined, + turnOff: false, + enabled: true, + serviceName: "", + serviceType: volumeBackupType, + }, + }); + + const serviceTypeForm = volumeBackupType; + const { data: destinations } = api.destination.all.useQuery(); + const { data: volumeBackup } = api.volumeBackups.one.useQuery( + { volumeBackupId: volumeBackupId || "" }, + { enabled: !!volumeBackupId }, + ); + + const { data: mounts } = api.mounts.allNamedByApplicationId.useQuery( + { applicationId: id || "" }, + { enabled: !!id && volumeBackupType === "application" }, + ); + + const { + data: services, + isFetching: isLoadingServices, + error: errorServices, + refetch: refetchServices, + } = api.compose.loadServices.useQuery( + { + composeId: id || "", + type: cacheType, + }, + { + retry: false, + refetchOnWindowFocus: false, + enabled: !!id && volumeBackupType === "compose", + }, + ); + + const serviceName = form.watch("serviceName"); + + const { data: mountsByService } = api.compose.loadMountsByService.useQuery( + { + composeId: id || "", + serviceName, + }, + { + enabled: !!id && volumeBackupType === "compose" && !!serviceName, + }, + ); + + useEffect(() => { + if (volumeBackupId && volumeBackup) { + form.reset({ + name: volumeBackup.name, + cronExpression: volumeBackup.cronExpression, + volumeName: volumeBackup.volumeName || "", + prefix: volumeBackup.prefix, + keepLatestCount: volumeBackup.keepLatestCount || undefined, + turnOff: volumeBackup.turnOff, + enabled: volumeBackup.enabled || false, + serviceName: volumeBackup.serviceName || "", + destinationId: volumeBackup.destinationId, + serviceType: volumeBackup.serviceType, + }); + setKeepLatestCountInput( + volumeBackup.keepLatestCount !== null && + volumeBackup.keepLatestCount !== undefined + ? String(volumeBackup.keepLatestCount) + : "", + ); + } + }, [form, volumeBackup, volumeBackupId]); + + const { mutateAsync, isLoading } = volumeBackupId + ? api.volumeBackups.update.useMutation() + : api.volumeBackups.create.useMutation(); + + const onSubmit = async (values: z.infer) => { + if (!id && !volumeBackupId) return; + + const preparedKeepLatestCount = + keepLatestCountInput === "" ? null : (values.keepLatestCount ?? null); + + await mutateAsync({ + ...values, + keepLatestCount: preparedKeepLatestCount, + destinationId: values.destinationId, + volumeBackupId: volumeBackupId || "", + serviceType: volumeBackupType, + ...(volumeBackupType === "application" && { + applicationId: id || "", + }), + ...(volumeBackupType === "compose" && { + composeId: id || "", + }), + ...(volumeBackupType === "postgres" && { + serverId: id || "", + }), + ...(volumeBackupType === "postgres" && { + postgresId: id || "", + }), + ...(volumeBackupType === "mariadb" && { + mariadbId: id || "", + }), + ...(volumeBackupType === "mongo" && { + mongoId: id || "", + }), + ...(volumeBackupType === "mysql" && { + mysqlId: id || "", + }), + ...(volumeBackupType === "redis" && { + redisId: id || "", + }), + }) + .then(() => { + toast.success( + `Volume backup ${volumeBackupId ? "updated" : "created"} successfully`, + ); + utils.volumeBackups.list.invalidate({ + id, + volumeBackupType, + }); + setIsOpen(false); + }) + .catch((error) => { + toast.error( + error instanceof Error ? error.message : "An unknown error occurred", + ); + }); + }; + + return ( +

+ + {volumeBackupId ? ( + + ) : ( + + )} + + + + + {volumeBackupId ? "Edit" : "Create"} Volume Backup + + + Create a volume backup to backup your volume to a destination + + + + + ( + + + Task Name + + + + + + A descriptive name for your scheduled task + + + + )} + /> + + + ( + + Destination + + + Choose the backup destination where files will be stored + + + + )} + /> + {serviceTypeForm === "compose" && ( + <> +
+ {errorServices && ( + + {errorServices?.message} + + )} + ( + + Service Name +
+ + + + + + + +

+ Fetch: Will clone the repository and load the + services +

+
+
+
+ + + + + + +

+ Cache: If you previously deployed this + compose, it will read the services from the + last deployment/fetch from the repository +

+
+
+
+
+ + +
+ )} + /> +
+ {mountsByService && mountsByService.length > 0 && ( + ( + + Volumes + + + Choose the volume to backup, if you dont see the + volume here, you can type the volume name manually + + + + )} + /> + )} + + )} + {serviceTypeForm === "application" && ( + ( + + Volumes + + + Choose the volume to backup, if you dont see the volume + here, you can type the volume name manually + + + + )} + /> + )} + + ( + + Volume Name + + + + + The name of the Docker volume to backup + + + + )} + /> + + ( + + Backup Prefix + + + + + Prefix for backup files (optional) + + + + )} + /> + + ( + + Keep Latest Backups + + { + const raw = e.target.value; + setKeepLatestCountInput(raw); + if (raw === "") { + field.onChange(undefined); + } else if (/^\d+$/.test(raw)) { + field.onChange(Number(raw)); + } + }} + /> + + + How many recent backups to keep. Empty means no cleanup. + + + + )} + /> + + ( + + + + Turn Off Container During Backup + + + โš ๏ธ The container will be temporarily stopped during backup to + prevent file corruption. This ensures data integrity but may + cause temporary service interruption. + + + )} + /> + + ( + + + + Enabled + + + )} + /> + + + + +
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/application/volume-backups/restore-volume-backups.tsx b/apps/dokploy/components/dashboard/application/volume-backups/restore-volume-backups.tsx new file mode 100644 index 0000000000..6eda336486 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/volume-backups/restore-volume-backups.tsx @@ -0,0 +1,411 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import copy from "copy-to-clipboard"; +import { debounce } from "lodash"; +import { CheckIcon, ChevronsUpDown, Copy, RotateCcw } 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 { DrawerLogs } from "@/components/shared/drawer-logs"; +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, + 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 { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { cn } from "@/lib/utils"; +import { api } from "@/utils/api"; +import { formatBytes } from "../../database/backups/restore-backup"; +import { type LogLine, parseLogs } from "../../docker/logs/utils"; + +interface Props { + id: string; + type: "application" | "compose"; + serverId?: string; +} + +const RestoreBackupSchema = z.object({ + destinationId: z + .string({ + required_error: "Please select a destination", + }) + .min(1, { + message: "Destination is required", + }), + backupFile: z + .string({ + required_error: "Please select a backup file", + }) + .min(1, { + message: "Backup file is required", + }), + volumeName: z + .string({ + required_error: "Please enter a volume name", + }) + .min(1, { + message: "Volume name is required", + }), +}); + +export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => { + const [isOpen, setIsOpen] = useState(false); + const [search, setSearch] = useState(""); + const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); + + const { data: destinations = [] } = api.destination.all.useQuery(); + + const form = useForm>({ + defaultValues: { + destinationId: "", + backupFile: "", + volumeName: "", + }, + resolver: zodResolver(RestoreBackupSchema), + }); + + const destinationId = form.watch("destinationId"); + const volumeName = form.watch("volumeName"); + const backupFile = form.watch("backupFile"); + + const debouncedSetSearch = debounce((value: string) => { + setDebouncedSearchTerm(value); + }, 350); + + const handleSearchChange = (value: string) => { + setSearch(value); + debouncedSetSearch(value); + }; + + const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery( + { + destinationId: destinationId, + search: debouncedSearchTerm, + serverId: serverId ?? "", + }, + { + enabled: isOpen && !!destinationId, + }, + ); + + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [filteredLogs, setFilteredLogs] = useState([]); + const [isDeploying, setIsDeploying] = useState(false); + + api.volumeBackups.restoreVolumeBackupWithLogs.useSubscription( + { + id, + serviceType: type, + serverId, + destinationId, + volumeName, + backupFileName: backupFile, + }, + { + enabled: isDeploying, + onData(log) { + if (!isDrawerOpen) { + setIsDrawerOpen(true); + } + + if (log === "Restore completed successfully!") { + setIsDeploying(false); + } + const parsedLogs = parseLogs(log); + setFilteredLogs((prev) => [...prev, ...parsedLogs]); + }, + onError(error) { + console.error("Restore logs error:", error); + setIsDeploying(false); + }, + }, + ); + + const onSubmit = async () => { + setIsDeploying(true); + }; + + return ( + + + + + + + + + Restore Volume Backup + + + Select a destination and search for volume backup files + + + Make sure the volume name is not being used by another container. + + + +
+ + ( + + Destination + + + + + + + + + + No destinations found. + + + {destinations.map((destination) => ( + { + form.setValue( + "destinationId", + destination.destinationId, + ); + }} + > + {destination.name} + + + ))} + + + + + + + + )} + /> + + ( + + + Search Backup Files + {field.value && ( + + {field.value} + { + e.stopPropagation(); + e.preventDefault(); + copy(field.value); + toast.success("Backup file copied to clipboard"); + }} + /> + + )} + + + + + + + + + + + {isLoading ? ( +
+ Loading backup files... +
+ ) : files.length === 0 && search ? ( +
+ No backup files found for "{search}" +
+ ) : files.length === 0 ? ( +
+ No backup files available +
+ ) : ( + + + {files?.map((file) => ( + { + form.setValue("backupFile", file.Path); + if (file.IsDir) { + setSearch(`${file.Path}/`); + setDebouncedSearchTerm(`${file.Path}/`); + } else { + setSearch(file.Path); + setDebouncedSearchTerm(file.Path); + } + }} + > +
+
+ + {file.Path} + + + +
+
+ + Size: {formatBytes(file.Size)} + + {file.IsDir && ( + + Directory + + )} + {file.Hashes?.MD5 && ( + MD5: {file.Hashes.MD5} + )} +
+
+
+ ))} +
+
+ )} +
+
+
+ +
+ )} + /> + ( + + Volume Name + + + + + + )} + /> + + + + + + + + { + setIsDrawerOpen(false); + setFilteredLogs([]); + setIsDeploying(false); + // refetch(); + }} + filteredLogs={filteredLogs} + /> +
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx b/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx new file mode 100644 index 0000000000..c88dd92f5e --- /dev/null +++ b/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx @@ -0,0 +1,250 @@ +import { + ClipboardList, + DatabaseBackup, + Loader2, + Play, + Trash2, +} from "lucide-react"; +import { toast } from "sonner"; +import { DialogAction } from "@/components/shared/dialog-action"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { api } from "@/utils/api"; +import { ShowDeploymentsModal } from "../deployments/show-deployments-modal"; +import { HandleVolumeBackups } from "./handle-volume-backups"; +import { RestoreVolumeBackups } from "./restore-volume-backups"; + +interface Props { + id: string; + type?: "application" | "compose"; + serverId?: string; +} + +export const ShowVolumeBackups = ({ + id, + type = "application", + serverId, +}: Props) => { + const { + data: volumeBackups, + isLoading: isLoadingVolumeBackups, + refetch: refetchVolumeBackups, + } = api.volumeBackups.list.useQuery( + { + id: id || "", + volumeBackupType: type, + }, + { + enabled: !!id, + }, + ); + + const utils = api.useUtils(); + + const { mutateAsync: deleteVolumeBackup, isLoading: isDeleting } = + api.volumeBackups.delete.useMutation(); + + const { mutateAsync: runManually, isLoading } = + api.volumeBackups.runManually.useMutation(); + + return ( + + +
+
+ + Volume Backups + + + Schedule volume backups to run automatically at specified + intervals. + +
+ +
+ {volumeBackups && volumeBackups.length > 0 && ( + <> + + +
+ +
+ + )} +
+
+
+ + {isLoadingVolumeBackups ? ( +
+ + + Loading volume backups... + +
+ ) : volumeBackups && volumeBackups.length > 0 ? ( +
+ {volumeBackups.map((volumeBackup) => { + const serverId = + volumeBackup.application?.serverId || + volumeBackup.postgres?.serverId || + volumeBackup.mysql?.serverId || + volumeBackup.mariadb?.serverId || + volumeBackup.mongo?.serverId || + volumeBackup.redis?.serverId || + volumeBackup.compose?.serverId; + return ( +
+
+
+ +
+
+
+

+ {volumeBackup.name} +

+ + {volumeBackup.enabled ? "Enabled" : "Disabled"} + +
+
+ + Cron: {volumeBackup.cronExpression} + +
+
+
+ +
+ + + + + + + + + + + Run Manual Volume Backup + + + + + + + { + await deleteVolumeBackup({ + volumeBackupId: volumeBackup.volumeBackupId, + }) + .then(() => { + utils.volumeBackups.list.invalidate({ + id, + volumeBackupType: type, + }); + toast.success("Volume backup deleted successfully"); + }) + .catch(() => { + toast.error("Error deleting volume backup"); + }); + }} + > + + +
+
+ ); + })} +
+ ) : ( +
+ +

+ No volume backups +

+

+ Create your first volume backup to automate your workflows +

+
+ + +
+
+ )} +
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx b/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx index c5a34b3c13..52eb189079 100644 --- a/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx +++ b/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx @@ -1,3 +1,8 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; import { @@ -18,11 +23,7 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; + interface Props { composeId: string; } diff --git a/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx b/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx new file mode 100644 index 0000000000..5b6e041544 --- /dev/null +++ b/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx @@ -0,0 +1,241 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { AlertTriangle, Loader2 } from "lucide-react"; +import { useEffect, 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 { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, +} from "@/components/ui/form"; + +import { Switch } from "@/components/ui/switch"; +import { api } from "@/utils/api"; + +interface Props { + composeId: string; +} + +// Schema for Isolated Deployment +const isolatedSchema = z.object({ + isolatedDeployment: z.boolean().optional(), +}); + +type IsolatedSchema = z.infer; + +export const IsolatedDeploymentTab = ({ composeId }: Props) => { + const utils = api.useUtils(); + const [compose, setCompose] = useState(""); + const [isPreviewLoading, setIsPreviewLoading] = useState(false); + const { mutateAsync, error, isError } = + api.compose.isolatedDeployment.useMutation(); + + const [isOpenPreview, setIsOpenPreview] = useState(false); + + const { mutateAsync: updateCompose } = api.compose.update.useMutation(); + + const { data, refetch } = api.compose.one.useQuery( + { composeId }, + { enabled: !!composeId }, + ); + + const form = useForm({ + defaultValues: { + isolatedDeployment: false, + }, + resolver: zodResolver(isolatedSchema), + }); + + useEffect(() => { + if (data) { + form.reset({ + isolatedDeployment: data?.isolatedDeployment || false, + }); + } + }, [form, form.reset, form.formState.isSubmitSuccessful, data]); + + const onSubmit = async (formData: IsolatedSchema) => { + await updateCompose({ + composeId, + isolatedDeployment: formData?.isolatedDeployment || false, + }) + .then(async (_data) => { + await refetch(); + toast.success("Compose updated"); + }) + .catch(() => { + toast.error("Error updating the compose"); + }); + }; + + const generatePreview = async () => { + setIsOpenPreview(true); + setIsPreviewLoading(true); + try { + await mutateAsync({ + composeId, + suffix: data?.appName || "", + }).then(async (data) => { + await utils.project.all.invalidate(); + setCompose(data); + }); + } catch { + toast.error("Error generating preview"); + setIsOpenPreview(false); + } finally { + setIsPreviewLoading(false); + } + }; + + return ( + + + Enable Isolated Deployment + + Configure isolated deployment to the compose file. +
+ + This feature creates an isolated environment for your deployment + by adding unique prefixes to all resources. It establishes a + dedicated network based on your compose file's name, ensuring your + services run in isolation. This prevents conflicts when running + multiple instances of the same template or services with identical + names. + +
+
+

+ Resources that will be isolated: +

+
    +
  • Docker networks
  • +
+
+
+
+
+
+ +
+ {isError && {error?.message}} +
+ + {isError && ( +
+ + + {error?.message} + +
+ )} + +
+
+ ( + +
+ + Enable Isolated Deployment ({data?.appName}) + + + Enable isolated deployment to the compose file. + +
+ + + +
+ )} + /> +
+ +
+ +
+
+ +
+ + + + + Isolated Deployment Preview + + Preview of the compose file with isolated deployment + configuration + + +
+ {isPreviewLoading ? ( +
+ +

+ Generating compose preview... +

+
+ ) : ( +
+													
+												
+ )} +
+
+
+
+
+ +
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/compose/delete-service.tsx b/apps/dokploy/components/dashboard/compose/delete-service.tsx index 212b5ac734..e75aad5e5e 100644 --- a/apps/dokploy/components/dashboard/compose/delete-service.tsx +++ b/apps/dokploy/components/dashboard/compose/delete-service.tsx @@ -1,3 +1,13 @@ +import type { ServiceType } from "@dokploy/server/db/schema"; +import { zodResolver } from "@hookform/resolvers/zod"; +import copy from "copy-to-clipboard"; +import { Copy, Trash2 } from "lucide-react"; +import { useRouter } from "next/router"; +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 { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; @@ -20,15 +30,6 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; -import type { ServiceType } from "@dokploy/server/db/schema"; -import { zodResolver } from "@hookform/resolvers/zod"; -import copy from "copy-to-clipboard"; -import { Copy, Trash2 } from "lucide-react"; -import { useRouter } from "next/router"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const deleteComposeSchema = z.object({ projectName: z.string().min(1, { @@ -100,7 +101,9 @@ export const DeleteService = ({ id, type }: Props) => { deleteVolumes, }) .then((result) => { - push(`/dashboard/project/${result?.projectId}`); + push( + `/dashboard/project/${result?.environment?.projectId}/environment/${result?.environment?.environmentId}`, + ); toast.success("deleted successfully"); setIsOpen(false); }) @@ -114,6 +117,12 @@ export const DeleteService = ({ id, type }: Props) => { } }; + const isDisabled = + (data && + "applicationStatus" in data && + data?.applicationStatus === "running") || + (data && "composeStatus" in data && data?.composeStatus === "running"); + return ( @@ -126,7 +135,7 @@ export const DeleteService = ({ id, type }: Props) => { - + Are you absolutely sure? @@ -202,6 +211,12 @@ export const DeleteService = ({ id, type }: Props) => {
+ {isDisabled && ( + + Cannot delete the service while it is running. Please wait for the + build to finish and then try again. + + )} + -
-
-
- -
-							
-						
-
- - - - ); -}; diff --git a/apps/dokploy/components/dashboard/compose/general/randomize-compose.tsx b/apps/dokploy/components/dashboard/compose/general/randomize-compose.tsx index 5ac67e0c88..2c488aefe6 100644 --- a/apps/dokploy/components/dashboard/compose/general/randomize-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/randomize-compose.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { AlertTriangle } from "lucide-react"; +import { useEffect, 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"; @@ -18,12 +24,6 @@ import { import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { AlertTriangle } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; interface Props { composeId: string; diff --git a/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx b/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx index 77f331bdd5..fac6c2a34e 100644 --- a/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx @@ -1,3 +1,6 @@ +import { Loader2, Puzzle, RefreshCw } from "lucide-react"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; import { AlertBlock } from "@/components/shared/alert-block"; import { CodeEditor } from "@/components/shared/code-editor"; import { Button } from "@/components/ui/button"; @@ -10,9 +13,6 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import { api } from "@/utils/api"; -import { Loader2, Puzzle, RefreshCw } from "lucide-react"; -import { useEffect, useState } from "react"; -import { toast } from "sonner"; interface Props { composeId: string; @@ -40,7 +40,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => { .then(() => { refetch(); }) - .catch((_err) => {}); + .catch(() => {}); } }, [isOpen]); @@ -52,7 +52,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => { Preview Compose - + Converted Compose @@ -62,7 +62,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => { {isError && {error?.message}} - + Preview your docker-compose file with added domains. Note: At least one domain must be specified for this conversion to take effect. @@ -79,7 +79,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {

) : ( <> -
+
- - - - Utilities - Modify the application data - - - - Isolated Deployment - Randomize Compose - - - - - - - - - -
- ); -}; diff --git a/apps/dokploy/components/dashboard/compose/general/show.tsx b/apps/dokploy/components/dashboard/compose/general/show.tsx index 71752525c5..4199363d89 100644 --- a/apps/dokploy/components/dashboard/compose/general/show.tsx +++ b/apps/dokploy/components/dashboard/compose/general/show.tsx @@ -9,6 +9,7 @@ import { import { api } from "@/utils/api"; import { ComposeActions } from "./actions"; import { ShowProviderFormCompose } from "./generic/show"; + interface Props { composeId: string; } diff --git a/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx index d166f933f3..4c004918bd 100644 --- a/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx +++ b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx @@ -1,3 +1,6 @@ +import { Loader2 } from "lucide-react"; +import dynamic from "next/dynamic"; +import { useEffect, useState } from "react"; import { badgeStateColor } from "@/components/dashboard/application/logs/show"; import { Badge } from "@/components/ui/badge"; import { @@ -19,9 +22,6 @@ import { } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; -import { Loader2 } from "lucide-react"; -import dynamic from "next/dynamic"; -import { useEffect, useState } from "react"; export const DockerLogs = dynamic( () => import("@/components/dashboard/docker/logs/docker-logs-id").then( diff --git a/apps/dokploy/components/dashboard/compose/logs/show.tsx b/apps/dokploy/components/dashboard/compose/logs/show.tsx index 571190549e..a4551f4156 100644 --- a/apps/dokploy/components/dashboard/compose/logs/show.tsx +++ b/apps/dokploy/components/dashboard/compose/logs/show.tsx @@ -1,3 +1,6 @@ +import { Loader2 } from "lucide-react"; +import dynamic from "next/dynamic"; +import { useEffect, useState } from "react"; import { badgeStateColor } from "@/components/dashboard/application/logs/show"; import { Badge } from "@/components/ui/badge"; import { @@ -18,9 +21,6 @@ import { SelectValue, } from "@/components/ui/select"; import { api } from "@/utils/api"; -import { Loader2 } from "lucide-react"; -import dynamic from "next/dynamic"; -import { useEffect, useState } from "react"; export const DockerLogs = dynamic( () => import("@/components/dashboard/docker/logs/docker-logs-id").then( diff --git a/apps/dokploy/components/dashboard/compose/update-compose.tsx b/apps/dokploy/components/dashboard/compose/update-compose.tsx index c896186018..7564988e2d 100644 --- a/apps/dokploy/components/dashboard/compose/update-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/update-compose.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { PenBoxIcon } from "lucide-react"; +import { useEffect, 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 { Button } from "@/components/ui/button"; import { @@ -20,12 +26,6 @@ import { import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { PenBoxIcon } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const updateComposeSchema = z.object({ name: z.string().min(1, { @@ -99,7 +99,7 @@ export const UpdateCompose = ({ composeId }: Props) => { - + Modify Compose Update the compose data diff --git a/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx b/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx index ca2cd27fbb..f2ca41b85e 100644 --- a/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx @@ -1,3 +1,16 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { + CheckIcon, + ChevronsUpDown, + DatabaseZap, + PenBoxIcon, + PlusIcon, + RefreshCw, +} from "lucide-react"; +import { useEffect, 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 { Button } from "@/components/ui/button"; import { @@ -48,20 +61,7 @@ import { } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { - DatabaseZap, - Info, - PenBoxIcon, - PlusIcon, - RefreshCw, -} from "lucide-react"; -import { CheckIcon, ChevronsUpDown } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; -import { commonCronExpressions } from "../../application/schedules/handle-schedules"; +import { ScheduleFormField } from "../../application/schedules/handle-schedules"; type CacheType = "cache" | "fetch"; @@ -329,7 +329,7 @@ export const HandleBackup = ({ )} - + {backupId ? "Update Backup" : "Create Backup"} @@ -578,66 +578,9 @@ export const HandleBackup = ({ ); }} /> - { - return ( - - - Schedule - - - - - - -

- Cron expression format: minute hour day month - weekday -

-

Example: 0 0 * * * (daily at midnight)

-
-
-
-
-
- -
- - - -
-
- - Choose a predefined schedule or enter a custom cron - expression - - -
- ); - }} - /> + + + { +export const formatBytes = (bytes: number): string => { if (bytes === 0) return "0 Bytes"; const k = 1024; const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; @@ -324,7 +324,7 @@ export const RestoreBackup = ({ Restore Backup - + @@ -415,7 +415,7 @@ export const RestoreBackup = ({ Search Backup Files {field.value && ( - + {field.value} - {field.value || "Search and select a backup file"} + + {field.value || "Search and select a backup file"} + diff --git a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx index 28ee68a9ce..55a09b25f1 100644 --- a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx +++ b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx @@ -1,3 +1,13 @@ +import { + ClipboardList, + Database, + DatabaseBackup, + Play, + Trash2, +} from "lucide-react"; +import Link from "next/link"; +import { useState } from "react"; +import { toast } from "sonner"; import { MariadbIcon, MongodbIcon, @@ -22,16 +32,6 @@ import { } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; -import { - ClipboardList, - Database, - DatabaseBackup, - Play, - Trash2, -} from "lucide-react"; -import Link from "next/link"; -import { useState } from "react"; -import { toast } from "sonner"; import type { ServiceType } from "../../application/advanced/show-resources"; import { ShowDeploymentsModal } from "../../application/deployments/show-deployments-modal"; import { HandleBackup } from "./handle-backup"; diff --git a/apps/dokploy/components/dashboard/docker/config/show-container-config.tsx b/apps/dokploy/components/dashboard/docker/config/show-container-config.tsx index 1f1591c9d0..2b93b1dbec 100644 --- a/apps/dokploy/components/dashboard/docker/config/show-container-config.tsx +++ b/apps/dokploy/components/dashboard/docker/config/show-container-config.tsx @@ -42,7 +42,7 @@ export const ShowContainerConfig = ({ containerId, serverId }: Props) => { See in detail the config of this container -
+
 							 = ({
 	const [showTimestamp, setShowTimestamp] = React.useState(true);
 	const [since, setSince] = React.useState("all");
 	const [typeFilter, setTypeFilter] = React.useState([]);
+	const [isPaused, setIsPaused] = React.useState(false);
+	const [messageBuffer, setMessageBuffer] = React.useState([]);
+	const isPausedRef = useRef(false);
 	const scrollRef = useRef(null);
 	const [isLoading, setIsLoading] = React.useState(false);
 
@@ -85,15 +89,38 @@ export const DockerLogsId: React.FC = ({
 	const handleLines = (lines: number) => {
 		setRawLogs("");
 		setFilteredLogs([]);
+		setMessageBuffer([]);
 		setLines(lines);
 	};
 
 	const handleSince = (value: TimeFilter) => {
 		setRawLogs("");
 		setFilteredLogs([]);
+		setMessageBuffer([]);
 		setSince(value);
 	};
 
+	const handlePauseResume = () => {
+		if (isPaused) {
+			// Resume: Apply all buffered messages
+			if (messageBuffer.length > 0) {
+				const bufferedContent = messageBuffer.join("");
+				setRawLogs((prev) => {
+					const updated = prev + bufferedContent;
+					const splitLines = updated.split("\n");
+					if (splitLines.length > lines) {
+						return splitLines.slice(-lines).join("\n");
+					}
+					return updated;
+				});
+				setMessageBuffer([]);
+			}
+		}
+		const newPausedState = !isPaused;
+		setIsPaused(newPausedState);
+		isPausedRef.current = newPausedState;
+	};
+
 	useEffect(() => {
 		if (!containerId) return;
 
@@ -102,6 +129,10 @@ export const DockerLogsId: React.FC = ({
 		setIsLoading(true);
 		setRawLogs("");
 		setFilteredLogs([]);
+		setMessageBuffer([]);
+		// Reset pause state when container changes
+		setIsPaused(false);
+		isPausedRef.current = false;
 
 		const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
 		const params = new globalThis.URLSearchParams({
@@ -140,14 +171,22 @@ export const DockerLogsId: React.FC = ({
 
 		ws.onmessage = (e) => {
 			if (!isCurrentConnection) return;
-			setRawLogs((prev) => {
-				const updated = prev + e.data;
-				const splitLines = updated.split("\n");
-				if (splitLines.length > lines) {
-					return splitLines.slice(-lines).join("\n");
-				}
-				return updated;
-			});
+
+			if (isPausedRef.current) {
+				// When paused, buffer the messages instead of displaying them
+				setMessageBuffer((prev) => [...prev, e.data]);
+			} else {
+				// When not paused, display messages normally
+				setRawLogs((prev) => {
+					const updated = prev + e.data;
+					const splitLines = updated.split("\n");
+					if (splitLines.length > lines) {
+						return splitLines.slice(-lines).join("\n");
+					}
+					return updated;
+				});
+			}
+
 			setIsLoading(false);
 			if (noDataTimeout) clearTimeout(noDataTimeout);
 		};
@@ -210,9 +249,15 @@ export const DockerLogsId: React.FC = ({
 		});
 	};
 
+	// Sync isPausedRef with isPaused state
+	useEffect(() => {
+		isPausedRef.current = isPaused;
+	}, [isPaused]);
+
 	useEffect(() => {
 		setRawLogs("");
 		setFilteredLogs([]);
+		setMessageBuffer([]);
 	}, [containerId]);
 
 	useEffect(() => {
@@ -260,17 +305,48 @@ export const DockerLogsId: React.FC = ({
 							/>
 						
- +
+ + +
+ {isPaused && ( + +
+ + + Logs paused + {messageBuffer.length > 0 && ( + + ({messageBuffer.length} messages buffered) + + )} + +
+
+ )}
import("@/components/dashboard/docker/logs/docker-logs-id").then( @@ -40,7 +40,7 @@ export const ShowDockerModalLogs = ({ {children} - + View Logs View the logs for {containerId} diff --git a/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-stack-logs.tsx b/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-stack-logs.tsx index 36719bb073..0399e2c675 100644 --- a/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-stack-logs.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-stack-logs.tsx @@ -1,3 +1,5 @@ +import dynamic from "next/dynamic"; +import type React from "react"; import { Dialog, DialogContent, @@ -7,8 +9,6 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; -import dynamic from "next/dynamic"; -import type React from "react"; export const DockerLogsId = dynamic( () => import("@/components/dashboard/docker/logs/docker-logs-id").then( @@ -40,7 +40,7 @@ export const ShowDockerModalStackLogs = ({ {children} - + View Logs View the logs for {containerId} diff --git a/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx b/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx index 44f2cdfc32..986a19059f 100644 --- a/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx @@ -1,3 +1,4 @@ +import { CheckIcon } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { @@ -14,7 +15,6 @@ import { import { Separator } from "@/components/ui/separator"; import { Switch } from "@/components/ui/switch"; import { cn } from "@/lib/utils"; -import { CheckIcon } from "lucide-react"; export type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h"; diff --git a/apps/dokploy/components/dashboard/docker/logs/status-logs-filter.tsx b/apps/dokploy/components/dashboard/docker/logs/status-logs-filter.tsx index 3ef11517ab..22c1ed6489 100644 --- a/apps/dokploy/components/dashboard/docker/logs/status-logs-filter.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/status-logs-filter.tsx @@ -1,3 +1,5 @@ +import { CheckIcon } from "lucide-react"; +import type React from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { @@ -13,8 +15,6 @@ import { } from "@/components/ui/popover"; import { Separator } from "@/components/ui/separator"; import { cn } from "@/lib/utils"; -import { CheckIcon } from "lucide-react"; -import type React from "react"; interface StatusLogsFilterProps { value?: string[]; diff --git a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx index 48ec4557be..5b929f3b6c 100644 --- a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx @@ -1,3 +1,5 @@ +import { FancyAnsi } from "fancy-ansi"; +import { escapeRegExp } from "lodash"; import { Badge } from "@/components/ui/badge"; import { Tooltip, @@ -7,9 +9,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; -import { FancyAnsi } from "fancy-ansi"; -import { escapeRegExp } from "lodash"; -import { type LogLine, getLogType } from "./utils"; +import { getLogType, type LogLine } from "./utils"; interface LogLineProps { log: LogLine; diff --git a/apps/dokploy/components/dashboard/docker/show/colums.tsx b/apps/dokploy/components/dashboard/docker/show/colums.tsx index 1cf0200f2c..74fe6819ed 100644 --- a/apps/dokploy/components/dashboard/docker/show/colums.tsx +++ b/apps/dokploy/components/dashboard/docker/show/colums.tsx @@ -1,6 +1,6 @@ import type { ColumnDef } from "@tanstack/react-table"; import { ArrowUpDown, MoreHorizontal } from "lucide-react"; - +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -8,8 +8,6 @@ import { DropdownMenuLabel, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; - -import { Badge } from "@/components/ui/badge"; import { ShowContainerConfig } from "../config/show-container-config"; import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs"; import { DockerTerminalModal } from "../terminal/docker-terminal-modal"; diff --git a/apps/dokploy/components/dashboard/docker/show/show-containers.tsx b/apps/dokploy/components/dashboard/docker/show/show-containers.tsx index 024b00618b..52398aabe1 100644 --- a/apps/dokploy/components/dashboard/docker/show/show-containers.tsx +++ b/apps/dokploy/components/dashboard/docker/show/show-containers.tsx @@ -1,3 +1,16 @@ +import { + type ColumnFiltersState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + type SortingState, + useReactTable, + type VisibilityState, +} from "@tanstack/react-table"; +import { ChevronDown, Container } from "lucide-react"; +import * as React from "react"; import { Button } from "@/components/ui/button"; import { Card, @@ -21,20 +34,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { type RouterOutputs, api } from "@/utils/api"; -import { - type ColumnFiltersState, - type SortingState, - type VisibilityState, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; -import { ChevronDown, Container } from "lucide-react"; -import * as React from "react"; +import { api, type RouterOutputs } from "@/utils/api"; import { columns } from "./colums"; export type Container = NonNullable< RouterOutputs["docker"]["getContainers"] 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 90aa2b4067..62c1347e40 100644 --- a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx +++ b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx @@ -1,3 +1,5 @@ +import dynamic from "next/dynamic"; +import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -9,8 +11,6 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; -import dynamic from "next/dynamic"; -import { useState } from "react"; const Terminal = dynamic( () => import("./docker-terminal").then((e) => e.DockerTerminal), @@ -60,7 +60,7 @@ export const DockerTerminalModal = ({ event.preventDefault()} > diff --git a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx index bf14680a46..ad34d69cef 100644 --- a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx +++ b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx @@ -2,9 +2,9 @@ import { Terminal } from "@xterm/xterm"; import React, { useEffect, useRef } from "react"; import { FitAddon } from "xterm-addon-fit"; import "@xterm/xterm/css/xterm.css"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { AttachAddon } from "@xterm/addon-attach"; import { useTheme } from "next-themes"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; interface Props { id: string; 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 fb5fe8f5c2..8c848a0dc5 100644 --- a/apps/dokploy/components/dashboard/file-system/show-traefik-file.tsx +++ b/apps/dokploy/components/dashboard/file-system/show-traefik-file.tsx @@ -1,7 +1,12 @@ -import { Button } from "@/components/ui/button"; - +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"; +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 { Form, FormControl, @@ -12,12 +17,6 @@ import { FormMessage, } 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"; -import { z } from "zod"; import { validateAndFormatYAML } from "../application/advanced/traefik/update-traefik-config"; const UpdateServerMiddlewareConfigSchema = z.object({ 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 c9272f293a..94a5c72a6d 100644 --- a/apps/dokploy/components/dashboard/file-system/show-traefik-system.tsx +++ b/apps/dokploy/components/dashboard/file-system/show-traefik-system.tsx @@ -1,3 +1,5 @@ +import { FileIcon, Folder, Loader2, Workflow } from "lucide-react"; +import React from "react"; import { AlertBlock } from "@/components/shared/alert-block"; import { Card, @@ -8,8 +10,6 @@ import { } from "@/components/ui/card"; import { Tree } from "@/components/ui/file-tree"; import { api } from "@/utils/api"; -import { FileIcon, Folder, Loader2, Workflow } from "lucide-react"; -import React from "react"; import { ShowTraefikFile } from "./show-traefik-file"; interface Props { diff --git a/apps/dokploy/components/dashboard/impersonation/impersonation-bar.tsx b/apps/dokploy/components/dashboard/impersonation/impersonation-bar.tsx index 8a9f55c902..7804e9adda 100644 --- a/apps/dokploy/components/dashboard/impersonation/impersonation-bar.tsx +++ b/apps/dokploy/components/dashboard/impersonation/impersonation-bar.tsx @@ -1,5 +1,24 @@ "use client"; +import copy from "copy-to-clipboard"; +import { format } from "date-fns"; +import { + Building2, + Calendar, + CheckIcon, + ChevronsUpDown, + Copy, + CreditCard, + Fingerprint, + Key, + Server, + Settings2, + Shield, + UserIcon, + XIcon, +} from "lucide-react"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; import { Logo } from "@/components/shared/logo"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; @@ -26,25 +45,6 @@ import { import { authClient } from "@/lib/auth-client"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; -import copy from "copy-to-clipboard"; -import { format } from "date-fns"; -import { - Building2, - Calendar, - CheckIcon, - ChevronsUpDown, - Copy, - CreditCard, - Fingerprint, - Key, - Server, - Settings2, - Shield, - UserIcon, - XIcon, -} from "lucide-react"; -import { useEffect, useState } from "react"; -import { toast } from "sonner"; type User = typeof authClient.$Infer.Session.user; diff --git a/apps/dokploy/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx b/apps/dokploy/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx index c00af42be8..8745db2868 100644 --- a/apps/dokploy/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx +++ b/apps/dokploy/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import Link from "next/link"; +import { useEffect, 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 { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { Button } from "@/components/ui/button"; @@ -19,12 +25,6 @@ import { import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import Link from "next/link"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const DockerProviderSchema = z.object({ externalPort: z.preprocess((a) => { @@ -102,9 +102,9 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => { External Credentials - In order to make the database reachable trought internet is - required to set a port, make sure the port is not used by another - application or database + In order to make the database reachable through the internet, you + must set a port and ensure that the port is not being used by + another application or database 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 2f8bab77be..8e996846f9 100644 --- a/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx +++ b/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx @@ -1,3 +1,7 @@ +import * as TooltipPrimitive from "@radix-ui/react-tooltip"; +import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; import { DialogAction } from "@/components/shared/dialog-action"; import { DrawerLogs } from "@/components/shared/drawer-logs"; import { Button } from "@/components/ui/button"; @@ -9,10 +13,6 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { api } from "@/utils/api"; -import * as TooltipPrimitive from "@radix-ui/react-tooltip"; -import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react"; -import { useState } from "react"; -import { toast } from "sonner"; import { type LogLine, parseLogs } from "../../docker/logs/utils"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; diff --git a/apps/dokploy/components/dashboard/mariadb/update-mariadb.tsx b/apps/dokploy/components/dashboard/mariadb/update-mariadb.tsx index 64705b693b..62486e0156 100644 --- a/apps/dokploy/components/dashboard/mariadb/update-mariadb.tsx +++ b/apps/dokploy/components/dashboard/mariadb/update-mariadb.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { PenBoxIcon } from "lucide-react"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; import { @@ -20,12 +26,6 @@ import { import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { PenBoxIcon } from "lucide-react"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const updateMariadbSchema = z.object({ name: z.string().min(1, { @@ -97,7 +97,7 @@ export const UpdateMariadb = ({ mariadbId }: Props) => { - + Modify MariaDB Update the MariaDB data diff --git a/apps/dokploy/components/dashboard/mongo/general/show-external-mongo-credentials.tsx b/apps/dokploy/components/dashboard/mongo/general/show-external-mongo-credentials.tsx index 75772bfdf2..d30061db54 100644 --- a/apps/dokploy/components/dashboard/mongo/general/show-external-mongo-credentials.tsx +++ b/apps/dokploy/components/dashboard/mongo/general/show-external-mongo-credentials.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import Link from "next/link"; +import { useEffect, 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 { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { Button } from "@/components/ui/button"; @@ -19,12 +25,6 @@ import { import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import Link from "next/link"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const DockerProviderSchema = z.object({ externalPort: z.preprocess((a) => { @@ -102,9 +102,9 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => { External Credentials - In order to make the database reachable trought internet is - required to set a port, make sure the port is not used by another - application or database + In order to make the database reachable through the internet, you + must set a port and ensure that the port is not being used by + another application or database diff --git a/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx b/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx index fdc28adc3f..23fbe51d30 100644 --- a/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx +++ b/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx @@ -1,3 +1,7 @@ +import * as TooltipPrimitive from "@radix-ui/react-tooltip"; +import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; import { DialogAction } from "@/components/shared/dialog-action"; import { DrawerLogs } from "@/components/shared/drawer-logs"; import { Button } from "@/components/ui/button"; @@ -9,12 +13,9 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { api } from "@/utils/api"; -import * as TooltipPrimitive from "@radix-ui/react-tooltip"; -import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react"; -import { useState } from "react"; -import { toast } from "sonner"; import { type LogLine, parseLogs } from "../../docker/logs/utils"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; + interface Props { mongoId: string; } diff --git a/apps/dokploy/components/dashboard/mongo/update-mongo.tsx b/apps/dokploy/components/dashboard/mongo/update-mongo.tsx index d42f406f0e..e78abddbd2 100644 --- a/apps/dokploy/components/dashboard/mongo/update-mongo.tsx +++ b/apps/dokploy/components/dashboard/mongo/update-mongo.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { PenBoxIcon } from "lucide-react"; +import { useEffect, 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 { Button } from "@/components/ui/button"; import { @@ -20,12 +26,6 @@ import { import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { PenBoxIcon } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const updateMongoSchema = z.object({ name: z.string().min(1, { @@ -99,7 +99,7 @@ export const UpdateMongo = ({ mongoId }: Props) => { - + Modify MongoDB Update the MongoDB data diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/docker-memory-chart.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/docker-memory-chart.tsx index 82a1ff3d55..34a3913a72 100644 --- a/apps/dokploy/components/dashboard/monitoring/free/container/docker-memory-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/free/container/docker-memory-chart.tsx @@ -10,6 +10,7 @@ import { } from "recharts"; import type { DockerStatsJSON } from "./show-free-container-monitoring"; import { convertMemoryToBytes } from "./show-free-container-monitoring"; + interface Props { acummulativeData: DockerStatsJSON["memory"]; memoryLimitGB: number; diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/docker-network-chart.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/docker-network-chart.tsx index cd6b7dfde5..5e2414ceac 100644 --- a/apps/dokploy/components/dashboard/monitoring/free/container/docker-network-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/free/container/docker-network-chart.tsx @@ -9,6 +9,7 @@ import { YAxis, } from "recharts"; import type { DockerStatsJSON } from "./show-free-container-monitoring"; + interface Props { acummulativeData: DockerStatsJSON["network"]; } diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/show-free-compose-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/show-free-compose-monitoring.tsx index 84510154c4..246ae296d3 100644 --- a/apps/dokploy/components/dashboard/monitoring/free/container/show-free-compose-monitoring.tsx +++ b/apps/dokploy/components/dashboard/monitoring/free/container/show-free-compose-monitoring.tsx @@ -1,3 +1,6 @@ +import { Loader2 } from "lucide-react"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; import { badgeStateColor } from "@/components/dashboard/application/logs/show"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -18,9 +21,6 @@ import { SelectValue, } from "@/components/ui/select"; import { api } from "@/utils/api"; -import { Loader2 } from "lucide-react"; -import { useEffect, useState } from "react"; -import { toast } from "sonner"; import { ContainerFreeMonitoring } from "./show-free-container-monitoring"; interface Props { diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx index 117fae3883..b28c4d9b67 100644 --- a/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx +++ b/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx @@ -1,7 +1,7 @@ +import { useEffect, useState } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Progress } from "@/components/ui/progress"; import { api } from "@/utils/api"; -import { useEffect, useState } from "react"; import { DockerBlockChart } from "./docker-block-chart"; import { DockerCpuChart } from "./docker-cpu-chart"; import { DockerDiskChart } from "./docker-disk-chart"; diff --git a/apps/dokploy/components/dashboard/monitoring/paid/container/container-block-chart.tsx b/apps/dokploy/components/dashboard/monitoring/paid/container/container-block-chart.tsx index 12af6b91db..32e30f62ab 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/container/container-block-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/container/container-block-chart.tsx @@ -1,3 +1,4 @@ +import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; import { Card, CardContent, @@ -13,7 +14,6 @@ import { ChartTooltip, } from "@/components/ui/chart"; import { formatTimestamp } from "@/lib/utils"; -import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; interface ContainerMetric { timestamp: string; diff --git a/apps/dokploy/components/dashboard/monitoring/paid/container/container-cpu-chart.tsx b/apps/dokploy/components/dashboard/monitoring/paid/container/container-cpu-chart.tsx index 445e03e12a..76b010c7c7 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/container/container-cpu-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/container/container-cpu-chart.tsx @@ -1,3 +1,4 @@ +import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; import { Card, CardContent, @@ -13,7 +14,6 @@ import { ChartTooltip, } from "@/components/ui/chart"; import { formatTimestamp } from "@/lib/utils"; -import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; interface ContainerMetric { timestamp: string; diff --git a/apps/dokploy/components/dashboard/monitoring/paid/container/container-memory-chart.tsx b/apps/dokploy/components/dashboard/monitoring/paid/container/container-memory-chart.tsx index 4da8642858..ff5e858432 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/container/container-memory-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/container/container-memory-chart.tsx @@ -1,3 +1,4 @@ +import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; import { Card, CardContent, @@ -13,7 +14,6 @@ import { ChartTooltip, } from "@/components/ui/chart"; import { formatTimestamp } from "@/lib/utils"; -import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; interface ContainerMetric { timestamp: string; diff --git a/apps/dokploy/components/dashboard/monitoring/paid/container/container-network-chart.tsx b/apps/dokploy/components/dashboard/monitoring/paid/container/container-network-chart.tsx index d51e896876..f962e2ae36 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/container/container-network-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/container/container-network-chart.tsx @@ -1,3 +1,4 @@ +import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; import { Card, CardContent, @@ -13,7 +14,6 @@ import { ChartTooltip, } from "@/components/ui/chart"; import { formatTimestamp } from "@/lib/utils"; -import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; interface ContainerMetric { timestamp: string; diff --git a/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring.tsx index 4ca461c215..0260438065 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring.tsx @@ -1,3 +1,6 @@ +import { Loader2 } from "lucide-react"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; import { badgeStateColor } from "@/components/dashboard/application/logs/show"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -19,9 +22,6 @@ import { SelectValue, } from "@/components/ui/select"; import { api } from "@/utils/api"; -import { Loader2 } from "lucide-react"; -import { useEffect, useState } from "react"; -import { toast } from "sonner"; import { ContainerPaidMonitoring } from "./show-paid-container-monitoring"; interface Props { diff --git a/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-container-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-container-monitoring.tsx index 3b189c2ac6..db087afa0e 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-container-monitoring.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-container-monitoring.tsx @@ -1,3 +1,5 @@ +import { Cpu, HardDrive, Loader2, MemoryStick, Network } from "lucide-react"; +import { useEffect, useState } from "react"; import { Card } from "@/components/ui/card"; import { Select, @@ -7,8 +9,6 @@ import { SelectValue, } from "@/components/ui/select"; import { api } from "@/utils/api"; -import { Cpu, HardDrive, Loader2, MemoryStick, Network } from "lucide-react"; -import { useEffect, useState } from "react"; import { ContainerBlockChart } from "./container-block-chart"; import { ContainerCPUChart } from "./container-cpu-chart"; import { ContainerMemoryChart } from "./container-memory-chart"; @@ -123,7 +123,7 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => { ? queryError.message : "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly."}

-

URL: {baseUrl}

+

URL: {baseUrl}

); diff --git a/apps/dokploy/components/dashboard/monitoring/paid/servers/cpu-chart.tsx b/apps/dokploy/components/dashboard/monitoring/paid/servers/cpu-chart.tsx index 8c9602ee22..efa84ffc48 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/servers/cpu-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/servers/cpu-chart.tsx @@ -1,3 +1,4 @@ +import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; import { Card, CardContent, @@ -13,7 +14,6 @@ import { ChartTooltip, } from "@/components/ui/chart"; import { formatTimestamp } from "@/lib/utils"; -import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; interface CPUChartProps { data: any[]; diff --git a/apps/dokploy/components/dashboard/monitoring/paid/servers/memory-chart.tsx b/apps/dokploy/components/dashboard/monitoring/paid/servers/memory-chart.tsx index f4079c46d4..1981dace32 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/servers/memory-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/servers/memory-chart.tsx @@ -1,3 +1,4 @@ +import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; import { Card, CardContent, @@ -11,7 +12,6 @@ import { ChartTooltip, } from "@/components/ui/chart"; import { formatTimestamp } from "@/lib/utils"; -import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; interface MemoryChartProps { data: any[]; diff --git a/apps/dokploy/components/dashboard/monitoring/paid/servers/network-chart.tsx b/apps/dokploy/components/dashboard/monitoring/paid/servers/network-chart.tsx index b84af0952d..bbb522fdc3 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/servers/network-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/servers/network-chart.tsx @@ -1,3 +1,4 @@ +import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; import { Card, CardContent, @@ -13,7 +14,6 @@ import { ChartTooltip, } from "@/components/ui/chart"; import { formatTimestamp } from "@/lib/utils"; -import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; interface NetworkChartProps { data: any[]; diff --git a/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx index e92ce03fc4..af0dacc1d9 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx @@ -1,3 +1,5 @@ +import { Clock, Cpu, HardDrive, Loader2, MemoryStick } from "lucide-react"; +import { useEffect, useState } from "react"; import { Select, SelectContent, @@ -6,8 +8,6 @@ import { SelectValue, } from "@/components/ui/select"; import { api } from "@/utils/api"; -import { Clock, Cpu, HardDrive, Loader2, MemoryStick } from "lucide-react"; -import { useEffect, useState } from "react"; import { CPUChart } from "./cpu-chart"; import { DiskChart } from "./disk-chart"; import { MemoryChart } from "./memory-chart"; @@ -143,7 +143,7 @@ export const ShowPaidMonitoring = ({ ? queryError.message : "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly."}

-

URL: {BASE_URL}

+

URL: {BASE_URL}

); diff --git a/apps/dokploy/components/dashboard/mysql/general/show-external-mysql-credentials.tsx b/apps/dokploy/components/dashboard/mysql/general/show-external-mysql-credentials.tsx index 73f99b7d07..dfaa36f6b9 100644 --- a/apps/dokploy/components/dashboard/mysql/general/show-external-mysql-credentials.tsx +++ b/apps/dokploy/components/dashboard/mysql/general/show-external-mysql-credentials.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import Link from "next/link"; +import { useEffect, 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 { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { Button } from "@/components/ui/button"; @@ -19,12 +25,6 @@ import { import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import Link from "next/link"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const DockerProviderSchema = z.object({ externalPort: z.preprocess((a) => { @@ -102,9 +102,9 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => { External Credentials - In order to make the database reachable trought internet is - required to set a port, make sure the port is not used by another - application or database + In order to make the database reachable through the internet, you + must set a port and ensure that the port is not being used by + another application or database diff --git a/apps/dokploy/components/dashboard/mysql/general/show-general-mysql.tsx b/apps/dokploy/components/dashboard/mysql/general/show-general-mysql.tsx index 590127fa79..045a717b7e 100644 --- a/apps/dokploy/components/dashboard/mysql/general/show-general-mysql.tsx +++ b/apps/dokploy/components/dashboard/mysql/general/show-general-mysql.tsx @@ -1,3 +1,7 @@ +import * as TooltipPrimitive from "@radix-ui/react-tooltip"; +import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; import { DialogAction } from "@/components/shared/dialog-action"; import { DrawerLogs } from "@/components/shared/drawer-logs"; import { Button } from "@/components/ui/button"; @@ -9,12 +13,9 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { api } from "@/utils/api"; -import * as TooltipPrimitive from "@radix-ui/react-tooltip"; -import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react"; -import { useState } from "react"; -import { toast } from "sonner"; import { type LogLine, parseLogs } from "../../docker/logs/utils"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; + interface Props { mysqlId: string; } diff --git a/apps/dokploy/components/dashboard/mysql/update-mysql.tsx b/apps/dokploy/components/dashboard/mysql/update-mysql.tsx index ec3c1b4544..353523aa01 100644 --- a/apps/dokploy/components/dashboard/mysql/update-mysql.tsx +++ b/apps/dokploy/components/dashboard/mysql/update-mysql.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { PenBoxIcon } from "lucide-react"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; import { @@ -20,12 +26,6 @@ import { import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { PenBoxIcon } from "lucide-react"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const updateMysqlSchema = z.object({ name: z.string().min(1, { @@ -97,7 +97,7 @@ export const UpdateMysql = ({ mysqlId }: Props) => { - + Modify MySQL Update the MySQL data diff --git a/apps/dokploy/components/dashboard/organization/handle-organization.tsx b/apps/dokploy/components/dashboard/organization/handle-organization.tsx index 014c37df18..c676e02333 100644 --- a/apps/dokploy/components/dashboard/organization/handle-organization.tsx +++ b/apps/dokploy/components/dashboard/organization/handle-organization.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { PenBoxIcon, Plus } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -18,13 +24,8 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { authClient } from "@/lib/auth-client"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { PenBoxIcon, Plus } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const organizationSchema = z.object({ name: z.string().min(1, { @@ -54,6 +55,8 @@ export function AddOrganization({ organizationId }: Props) { const { mutateAsync, isLoading } = organizationId ? api.organization.update.useMutation() : api.organization.create.useMutation(); + const { refetch: refetchActiveOrganization } = + authClient.useActiveOrganization(); const form = useForm({ resolver: zodResolver(organizationSchema), @@ -84,6 +87,10 @@ export function AddOrganization({ organizationId }: Props) { `Organization ${organizationId ? "updated" : "created"} successfully`, ); utils.organization.all.invalidate(); + if (organizationId) { + utils.organization.one.invalidate({ organizationId }); + refetchActiveOrganization(); + } setOpen(false); }) .catch((error) => { @@ -155,7 +162,7 @@ export function AddOrganization({ organizationId }: Props) { control={form.control} name="logo" render={({ field }) => ( - + Logo URL )} /> - + diff --git a/apps/dokploy/components/dashboard/postgres/advanced/show-custom-command.tsx b/apps/dokploy/components/dashboard/postgres/advanced/show-custom-command.tsx index 40e84844fa..febaa86447 100644 --- a/apps/dokploy/components/dashboard/postgres/advanced/show-custom-command.tsx +++ b/apps/dokploy/components/dashboard/postgres/advanced/show-custom-command.tsx @@ -1,3 +1,8 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { @@ -10,11 +15,6 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; import type { ServiceType } from "../../application/advanced/show-resources"; const addDockerImage = z.object({ diff --git a/apps/dokploy/components/dashboard/postgres/general/show-external-postgres-credentials.tsx b/apps/dokploy/components/dashboard/postgres/general/show-external-postgres-credentials.tsx index 444fa0ceec..46b3772a03 100644 --- a/apps/dokploy/components/dashboard/postgres/general/show-external-postgres-credentials.tsx +++ b/apps/dokploy/components/dashboard/postgres/general/show-external-postgres-credentials.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import Link from "next/link"; +import { useEffect, 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 { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { Button } from "@/components/ui/button"; @@ -19,12 +25,6 @@ import { import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import Link from "next/link"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const DockerProviderSchema = z.object({ externalPort: z.preprocess((a) => { @@ -104,9 +104,9 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => { External Credentials - In order to make the database reachable trought internet is - required to set a port, make sure the port is not used by another - application or database + In order to make the database reachable through the internet, you + must set a port and ensure that the port is not being used by + another application or database diff --git a/apps/dokploy/components/dashboard/postgres/general/show-general-postgres.tsx b/apps/dokploy/components/dashboard/postgres/general/show-general-postgres.tsx index fec51b5a2a..de520053d9 100644 --- a/apps/dokploy/components/dashboard/postgres/general/show-general-postgres.tsx +++ b/apps/dokploy/components/dashboard/postgres/general/show-general-postgres.tsx @@ -1,3 +1,7 @@ +import * as TooltipPrimitive from "@radix-ui/react-tooltip"; +import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; import { DialogAction } from "@/components/shared/dialog-action"; import { DrawerLogs } from "@/components/shared/drawer-logs"; import { Button } from "@/components/ui/button"; @@ -9,10 +13,6 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { api } from "@/utils/api"; -import * as TooltipPrimitive from "@radix-ui/react-tooltip"; -import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react"; -import { useState } from "react"; -import { toast } from "sonner"; import { type LogLine, parseLogs } from "../../docker/logs/utils"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; diff --git a/apps/dokploy/components/dashboard/postgres/update-postgres.tsx b/apps/dokploy/components/dashboard/postgres/update-postgres.tsx index f70cd8c90d..d4485862e4 100644 --- a/apps/dokploy/components/dashboard/postgres/update-postgres.tsx +++ b/apps/dokploy/components/dashboard/postgres/update-postgres.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { PenBox } from "lucide-react"; +import { useEffect, 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 { Button } from "@/components/ui/button"; import { @@ -20,12 +26,6 @@ import { import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { PenBox } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const updatePostgresSchema = z.object({ name: z.string().min(1, { @@ -99,7 +99,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => { - + Modify Postgres Update the Postgres data diff --git a/apps/dokploy/components/dashboard/project/add-ai-assistant.tsx b/apps/dokploy/components/dashboard/project/add-ai-assistant.tsx index 2bb47618ed..88fd1d111b 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 c93de25198..079701eb8b 100644 --- a/apps/dokploy/components/dashboard/project/add-application.tsx +++ b/apps/dokploy/components/dashboard/project/add-application.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { Folder, 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 { Button } from "@/components/ui/button"; import { @@ -37,12 +43,6 @@ import { } from "@/components/ui/tooltip"; import { slugify } from "@/lib/slug"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Folder, HelpCircle } from "lucide-react"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const AddTemplateSchema = z.object({ name: z.string().min(1, { @@ -64,17 +64,23 @@ 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); const slug = slugify(projectName); const { data: servers } = api.server.withSSHKey.useQuery(); + 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; + const { mutateAsync, isLoading, error, isError } = api.application.create.useMutation(); @@ -92,18 +98,18 @@ export const AddApplication = ({ projectId, projectName }: Props) => { name: data.name, appName: data.appName, description: data.description, - projectId, - serverId: data.serverId, + serverId: data.serverId === "dokploy" ? undefined : data.serverId, + environmentId, }) .then(async () => { toast.success("Service Created"); form.reset(); setVisible(false); - await utils.project.one.invalidate({ - projectId, + await utils.environment.one.invalidate({ + environmentId, }); }) - .catch((_e) => { + .catch(() => { toast.error("Error creating the service"); }); }; @@ -119,7 +125,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => { Application - + Create @@ -155,68 +161,100 @@ export const AddApplication = ({ projectId, projectName }: Props) => { )} /> - ( - - - - - - Select a Server {!isCloud ? "(Optional)" : ""} - - - - - - If no server is selected, the application will be - deployed on the server where the user is logged in. - - - - + {shouldShowServerDropdown && ( + ( + + + + + + Select a Server {!isCloud ? "(Optional)" : ""} + + + + + + If no server is selected, the application will be + deployed on the server where the user is logged in. + + + + - + + + + + + {!isCloud && ( + + + Dokploy + + Default + - - - ))} - Servers ({servers?.length}) - - - - - - )} - /> + + )} + {servers?.map((server) => ( + + + {server.name} + + {server.ipAddress} + + + + ))} + + Servers ({servers?.length + (!isCloud ? 1 : 0)}) + + + + + + + )} + /> + )} ( - App Name + + App Name + + + + + + +

+ This will be the name of the Docker Swarm service +

+
+
+
+
diff --git a/apps/dokploy/components/dashboard/project/add-compose.tsx b/apps/dokploy/components/dashboard/project/add-compose.tsx index 5f2bb137fa..a187104ecb 100644 --- a/apps/dokploy/components/dashboard/project/add-compose.tsx +++ b/apps/dokploy/components/dashboard/project/add-compose.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { CircuitBoard, HelpCircle } from "lucide-react"; +import { useEffect, 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 { Button } from "@/components/ui/button"; import { @@ -37,12 +43,6 @@ import { } from "@/components/ui/tooltip"; import { slugify } from "@/lib/slug"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { CircuitBoard, HelpCircle } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const AddComposeSchema = z.object({ composeType: z.enum(["docker-compose", "stack"]).optional(), @@ -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,15 @@ 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; + // 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; + const form = useForm({ defaultValues: { name: "", @@ -96,16 +105,17 @@ 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, + serverId: data.serverId === "dokploy" ? undefined : data.serverId, }) .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(() => { @@ -124,7 +134,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => { Compose - + Create Compose @@ -163,62 +173,80 @@ export const AddCompose = ({ projectId, projectName }: Props) => { )} /> - ( - - - - - - Select a Server {!isCloud ? "(Optional)" : ""} - - - - - - If no server is selected, the application will be - deployed on the server where the user is logged in. - - - - + {shouldShowServerDropdown && ( + ( + + + + + + Select a Server {!isCloud ? "(Optional)" : ""} + + + + + + If no server is selected, the application will be + deployed on the server where the user is logged in. + + + + - + + + + + + {!isCloud && ( + + + Dokploy + + Default + - - - ))} - Servers ({servers?.length}) - - - - - - )} - /> + + )} + {servers?.map((server) => ( + + + {server.name} + + {server.ipAddress} + + + + ))} + + Servers ({servers?.length + (!isCloud ? 1 : 0)}) + + + + + + + )} + /> + )} ?~`]*$/, { + message: + "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", + }), dockerImage: z.string(), description: z.string().nullable(), serverId: z.string().nullable(), @@ -106,7 +117,13 @@ const mySchema = z.discriminatedUnion("type", [ z .object({ type: z.literal("mysql"), - databaseRootPassword: z.string().default(""), + databaseRootPassword: z + .string() + .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { + message: + "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", + }) + .optional(), databaseUser: z.string().default("mysql"), databaseName: z.string().default("mysql"), }) @@ -115,7 +132,13 @@ const mySchema = z.discriminatedUnion("type", [ .object({ type: z.literal("mariadb"), dockerImage: z.string().default("mariadb:4"), - databaseRootPassword: z.string().default(""), + databaseRootPassword: z + .string() + .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { + message: + "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", + }) + .optional(), databaseUser: z.string().default("mariadb"), databaseName: z.string().default("mariadb"), }) @@ -148,14 +171,15 @@ 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); + const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: servers } = api.server.withSSHKey.useQuery(); const postgresMutation = api.postgres.create.useMutation(); const mongoMutation = api.mongo.create.useMutation(); @@ -163,6 +187,15 @@ 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; + // 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; + const form = useForm({ defaultValues: { type: "postgres", @@ -195,8 +228,8 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { name: data.name, appName: data.appName, dockerImage: defaultDockerImage, - projectId, - serverId: data.serverId, + serverId: data.serverId === "dokploy" ? undefined : data.serverId, + environmentId, description: data.description, }; @@ -208,7 +241,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], - serverId: data.serverId, + serverId: data.serverId === "dokploy" ? null : data.serverId, }); } else if (data.type === "mongo") { promise = mongoMutation.mutateAsync({ @@ -216,25 +249,24 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { databasePassword: data.databasePassword, databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], - serverId: data.serverId, + serverId: data.serverId === "dokploy" ? null : data.serverId, replicaSets: data.replicaSets, }); } else if (data.type === "redis") { promise = redisMutation.mutateAsync({ ...commonParams, databasePassword: data.databasePassword, - serverId: data.serverId, - projectId, + serverId: data.serverId === "dokploy" ? null : data.serverId, }); } else if (data.type === "mariadb") { promise = mariadbMutation.mutateAsync({ ...commonParams, databasePassword: data.databasePassword, - databaseRootPassword: data.databaseRootPassword, + databaseRootPassword: data.databaseRootPassword || "", databaseName: data.databaseName || "mariadb", databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], - serverId: data.serverId, + serverId: data.serverId === "dokploy" ? null : data.serverId, }); } else if (data.type === "mysql") { promise = mysqlMutation.mutateAsync({ @@ -243,8 +275,8 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { databaseName: data.databaseName || "mysql", databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], - databaseRootPassword: data.databaseRootPassword, - serverId: data.serverId, + serverId: data.serverId === "dokploy" ? null : data.serverId, + databaseRootPassword: data.databaseRootPassword || "", }); } @@ -263,8 +295,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(() => { @@ -283,7 +316,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { Database - + Databases @@ -374,45 +407,78 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
)} /> - ( - - Select a Server - - - - )} - /> + {shouldShowServerDropdown && ( + ( + + Select a Server + + + + )} + /> + )} ( - App Name + + App Name + + + + + + +

+ This will be the name of the Docker Swarm + service +

+
+
+
+
diff --git a/apps/dokploy/components/dashboard/project/add-template.tsx b/apps/dokploy/components/dashboard/project/add-template.tsx index 8e9de54d94..72c42da491 100644 --- a/apps/dokploy/components/dashboard/project/add-template.tsx +++ b/apps/dokploy/components/dashboard/project/add-template.tsx @@ -1,3 +1,18 @@ +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 { @@ -54,30 +69,15 @@ import { } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; -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"; 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) { @@ -137,6 +140,12 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => { 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 ( @@ -148,7 +157,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => { Template - +
@@ -162,7 +171,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => { setQuery(e.target.value)} - className="w-full sm:w-[200px]" + className="w-full" value={query} /> { onClick={() => setViewMode(viewMode === "detailed" ? "icon" : "detailed") } - className="h-9 w-9" + className="h-9 w-9 flex-shrink-0" > {viewMode === "detailed" ? ( @@ -305,9 +314,9 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => { : "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6", )} > - {templates?.map((template) => ( + {templates?.map((template, idx) => (
{ project. -
- - - - - - - - If no server is selected, the application - will be deployed on the server where the - user is logged in. - - - - + {shouldShowServerDropdown && ( +
+ + + + + + + + If no server is selected, the + application will be deployed on the + server where the user is logged in. + + + + - { + setServerId(e); + }} + defaultValue={ + !isCloud ? "dokploy" : undefined + } + > + + + + + + {!isCloud && ( + + + Dokploy + + Default + - - - ))} - - Servers ({servers?.length}) - - - - -
+ + )} + {servers?.map((server) => ( + + + {server.name} + + {server.ipAddress} + + + + ))} + + Servers ( + {servers?.length + (!isCloud ? 1 : 0)}) + + + + +
+ )} Cancel @@ -486,16 +515,20 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => { disabled={isLoading} onClick={async () => { const promise = mutateAsync({ - projectId, - serverId: serverId || undefined, + serverId: + serverId === "dokploy" + ? undefined + : serverId, + environmentId, id: template.id, baseUrl: customBaseUrl, }); 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 0000000000..d6497fd0f7 --- /dev/null +++ b/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx @@ -0,0 +1,446 @@ +import type { findEnvironmentsByProjectId } from "@dokploy/server"; +import { + ChevronDownIcon, + PencilIcon, + PlusIcon, + Terminal, + TrashIcon, +} from "lucide-react"; +import { useRouter } from "next/router"; +import { useState } from "react"; +import { toast } from "sonner"; +import { EnvironmentVariables } from "@/components/dashboard/project/environment-variables"; +import { AlertBlock } from "@/components/shared/alert-block"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { api } from "@/utils/api"; + +type Environment = Awaited< + ReturnType +>[number]; +interface AdvancedEnvironmentSelectorProps { + projectId: string; + currentEnvironmentId?: string; +} + +export const AdvancedEnvironmentSelector = ({ + projectId, + 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); + + const { data: environments } = api.environment.byProjectId.useQuery( + { projectId: projectId }, + { + enabled: !!projectId, + }, + ); + + // Form states + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + + // API mutations + const { data: environment } = api.environment.one.useQuery( + { environmentId: currentEnvironmentId || "" }, + { + enabled: !!currentEnvironmentId, + }, + ); + + const haveServices = + selectedEnvironment && + ((selectedEnvironment?.mariadb?.length || 0) > 0 || + (selectedEnvironment?.mongo?.length || 0) > 0 || + (selectedEnvironment?.mysql?.length || 0) > 0 || + (selectedEnvironment?.postgres?.length || 0) > 0 || + (selectedEnvironment?.redis?.length || 0) > 0 || + (selectedEnvironment?.applications?.length || 0) > 0 || + (selectedEnvironment?.compose?.length || 0) > 0); + 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.environment.byProjectId.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.environment.byProjectId.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.environment.byProjectId.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) => { + const servicesCount = + environment.mariadb.length + + environment.mongo.length + + environment.mysql.length + + environment.postgres.length + + environment.redis.length + + environment.applications.length + + environment.compose.length; + return ( +
+ { + router.push( + `/dashboard/project/${projectId}/environment/${environment.environmentId}`, + ); + }} + > +
+ + {environment.name} ({servicesCount}) + + {environment.environmentId === currentEnvironmentId && ( +
+ )} +
+ + + {/* Action buttons for non-production environments */} + + + + {environment.name !== "production" && ( +
+ + + +
+ )} +
+ ); + })} + + + setIsCreateDialogOpen(true)} + > + + Create Environment + + + + + + + + Create Environment + + Create a new environment for your project. + + + +
+
+ + setName(e.target.value)} + placeholder="Environment name" + /> +
+
+ +