diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..0b849afc0 --- /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 000000000..1d7be47ca 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 e3f6aa234..248b98d5a 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/dokploy.yml b/.github/workflows/dokploy.yml index afb4ba4d2..529cd8f7f 100644 --- a/.github/workflows/dokploy.yml +++ b/.github/workflows/dokploy.yml @@ -2,7 +2,7 @@ name: Dokploy Docker Build on: push: - branches: [main, canary] + branches: [main, canary, "fix/re-apply-database-migration-fix"] workflow_dispatch: env: diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index e9591f3cc..6c74dbc02 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -4,9 +4,15 @@ on: pull_request: branches: [main, canary] +permissions: + contents: read + jobs: - lint-and-typecheck: + pr-check: runs-on: ubuntu-latest + strategy: + matrix: + job: [build, test, typecheck] steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 @@ -15,32 +21,5 @@ jobs: node-version: 20.16.0 cache: "pnpm" - run: pnpm install --frozen-lockfile - - run: pnpm run server:build - - run: pnpm typecheck - - 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: - 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 test + - run: pnpm server:build + - run: pnpm ${{ matrix.job }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0ac5a3581..38a36345e 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 4d18a99ab..11310b18e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -58,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/README.md b/README.md index bd27474e0..8faf22a35 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
- Dokploy - Open Source Alternative to Vercel, Heroku and Netlify. + Dokploy - Open Source Alternative to Vercel, Heroku and Netlify.

@@ -11,9 +11,26 @@

+ + +
+ 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. @@ -43,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. @@ -95,7 +112,6 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com). ### Community Backers 🤝 - #### Organizations: [Sponsors on Open Collective](https://opencollective.com/dokploy) @@ -107,15 +123,15 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com). ### Contributors 🤝 - + Contributors -## Video Tutorial +## 📺 Video Tutorial 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 f0d22fedf..dfc2a355d 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -9,6 +9,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "inngest": "3.40.1", "@dokploy/server": "workspace:*", "@hono/node-server": "^1.14.3", "@hono/zod-validator": "0.3.0", diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 0db565995..8ddb56dec 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); + + 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, + ); + } }); +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, + ); + } + }, +); + app.get("/health", async (c) => { return c.json({ status: "ok" }); }); -const queue = new Queue({ - name: "deployments", - process: async (job: DeployJob) => { - logger.info("Deploying job", job); - return await deploy(job); - }, - redisClient, -}); - -(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 609289bf7..5a4355956 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 3f3c9698b..ee2ac3e50 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/__test__/compose/compose.test.ts b/apps/dokploy/__test__/compose/compose.test.ts index 9d4ba20f5..69d3a5212 100644 --- a/apps/dokploy/__test__/compose/compose.test.ts +++ b/apps/dokploy/__test__/compose/compose.test.ts @@ -1,5 +1,5 @@ -import { addSuffixToAllProperties } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToAllProperties } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/config/config-root.test.ts b/apps/dokploy/__test__/compose/config/config-root.test.ts index 4b40c073e..668e17902 100644 --- a/apps/dokploy/__test__/compose/config/config-root.test.ts +++ b/apps/dokploy/__test__/compose/config/config-root.test.ts @@ -1,6 +1,5 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToConfigsRoot } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToConfigsRoot, generateRandomHash } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/config/config-service.test.ts b/apps/dokploy/__test__/compose/config/config-service.test.ts index de014eb5e..246872f09 100644 --- a/apps/dokploy/__test__/compose/config/config-service.test.ts +++ b/apps/dokploy/__test__/compose/config/config-service.test.ts @@ -1,6 +1,8 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToConfigsInServices } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { + addSuffixToConfigsInServices, + generateRandomHash, +} from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/config/config.test.ts b/apps/dokploy/__test__/compose/config/config.test.ts index aed3350f5..2d5feeb9a 100644 --- a/apps/dokploy/__test__/compose/config/config.test.ts +++ b/apps/dokploy/__test__/compose/config/config.test.ts @@ -1,6 +1,5 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToAllConfigs } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToAllConfigs, generateRandomHash } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/domain/labels.test.ts b/apps/dokploy/__test__/compose/domain/labels.test.ts index 172bff2af..9a75e0a84 100644 --- a/apps/dokploy/__test__/compose/domain/labels.test.ts +++ b/apps/dokploy/__test__/compose/domain/labels.test.ts @@ -108,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 980502fff..c55f6fa86 100644 --- a/apps/dokploy/__test__/compose/network/network-root.test.ts +++ b/apps/dokploy/__test__/compose/network/network-root.test.ts @@ -1,6 +1,5 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToNetworksRoot } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToNetworksRoot, generateRandomHash } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/network/network-service.test.ts b/apps/dokploy/__test__/compose/network/network-service.test.ts index ee07d9de9..3cf46d4ab 100644 --- a/apps/dokploy/__test__/compose/network/network-service.test.ts +++ b/apps/dokploy/__test__/compose/network/network-service.test.ts @@ -1,6 +1,8 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToServiceNetworks } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { + addSuffixToServiceNetworks, + generateRandomHash, +} from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/network/network.test.ts b/apps/dokploy/__test__/compose/network/network.test.ts index 39cf03958..7ba1c6a83 100644 --- a/apps/dokploy/__test__/compose/network/network.test.ts +++ b/apps/dokploy/__test__/compose/network/network.test.ts @@ -1,10 +1,10 @@ -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"; diff --git a/apps/dokploy/__test__/compose/secrets/secret-root.test.ts b/apps/dokploy/__test__/compose/secrets/secret-root.test.ts index 1b1898c59..b8cef56e4 100644 --- a/apps/dokploy/__test__/compose/secrets/secret-root.test.ts +++ b/apps/dokploy/__test__/compose/secrets/secret-root.test.ts @@ -1,6 +1,5 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToSecretsRoot } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToSecretsRoot, generateRandomHash } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/secrets/secret-services.test.ts b/apps/dokploy/__test__/compose/secrets/secret-services.test.ts index 5206bbbaf..e12f611d0 100644 --- a/apps/dokploy/__test__/compose/secrets/secret-services.test.ts +++ b/apps/dokploy/__test__/compose/secrets/secret-services.test.ts @@ -1,6 +1,8 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToSecretsInServices } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { + addSuffixToSecretsInServices, + generateRandomHash, +} from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/secrets/secret.test.ts b/apps/dokploy/__test__/compose/secrets/secret.test.ts index d874dc5e7..3ff524ad7 100644 --- a/apps/dokploy/__test__/compose/secrets/secret.test.ts +++ b/apps/dokploy/__test__/compose/secrets/secret.test.ts @@ -1,5 +1,5 @@ -import { addSuffixToAllSecrets } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToAllSecrets } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; 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 bcb51fd04..6ad45c588 100644 --- a/apps/dokploy/__test__/compose/service/service-container-name.test.ts +++ b/apps/dokploy/__test__/compose/service/service-container-name.test.ts @@ -1,6 +1,5 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToServiceNames } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; 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 b27414be5..14a5789c4 100644 --- a/apps/dokploy/__test__/compose/service/service-depends-on.test.ts +++ b/apps/dokploy/__test__/compose/service/service-depends-on.test.ts @@ -1,6 +1,5 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToServiceNames } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/service/service-extends.test.ts b/apps/dokploy/__test__/compose/service/service-extends.test.ts index 8309a32fd..0b7e92c53 100644 --- a/apps/dokploy/__test__/compose/service/service-extends.test.ts +++ b/apps/dokploy/__test__/compose/service/service-extends.test.ts @@ -1,6 +1,5 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToServiceNames } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/service/service-links.test.ts b/apps/dokploy/__test__/compose/service/service-links.test.ts index 5f9b01ab2..6c8cde39e 100644 --- a/apps/dokploy/__test__/compose/service/service-links.test.ts +++ b/apps/dokploy/__test__/compose/service/service-links.test.ts @@ -1,6 +1,5 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToServiceNames } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/service/service-names.test.ts b/apps/dokploy/__test__/compose/service/service-names.test.ts index 936a32ecc..c65299b03 100644 --- a/apps/dokploy/__test__/compose/service/service-names.test.ts +++ b/apps/dokploy/__test__/compose/service/service-names.test.ts @@ -1,6 +1,5 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToServiceNames } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/service/service.test.ts b/apps/dokploy/__test__/compose/service/service.test.ts index c6050f75a..38895e073 100644 --- a/apps/dokploy/__test__/compose/service/service.test.ts +++ b/apps/dokploy/__test__/compose/service/service.test.ts @@ -1,8 +1,8 @@ +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"; 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 8066a6dd7..8aa8296e8 100644 --- a/apps/dokploy/__test__/compose/service/sevice-volumes-from.test.ts +++ b/apps/dokploy/__test__/compose/service/sevice-volumes-from.test.ts @@ -1,6 +1,5 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToServiceNames } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/volume/volume-2.test.ts b/apps/dokploy/__test__/compose/volume/volume-2.test.ts index 61cba82d3..6aa9d01d3 100644 --- a/apps/dokploy/__test__/compose/volume/volume-2.test.ts +++ b/apps/dokploy/__test__/compose/volume/volume-2.test.ts @@ -1,6 +1,9 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToAllVolumes, addSuffixToVolumesRoot } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { + addSuffixToAllVolumes, + addSuffixToVolumesRoot, + generateRandomHash, +} from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/volume/volume-root.test.ts b/apps/dokploy/__test__/compose/volume/volume-root.test.ts index d91cb64d3..80db1f0cc 100644 --- a/apps/dokploy/__test__/compose/volume/volume-root.test.ts +++ b/apps/dokploy/__test__/compose/volume/volume-root.test.ts @@ -1,6 +1,5 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToVolumesRoot } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToVolumesRoot, generateRandomHash } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/volume/volume-services.test.ts b/apps/dokploy/__test__/compose/volume/volume-services.test.ts index 04a1a45ae..0e9cb018f 100644 --- a/apps/dokploy/__test__/compose/volume/volume-services.test.ts +++ b/apps/dokploy/__test__/compose/volume/volume-services.test.ts @@ -1,6 +1,8 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToVolumesInServices } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { + addSuffixToVolumesInServices, + generateRandomHash, +} from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/volume/volume.test.ts b/apps/dokploy/__test__/compose/volume/volume.test.ts index 6c4344762..6f8e76708 100644 --- a/apps/dokploy/__test__/compose/volume/volume.test.ts +++ b/apps/dokploy/__test__/compose/volume/volume.test.ts @@ -1,5 +1,5 @@ -import { addSuffixToAllVolumes } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToAllVolumes } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/deploy/github.test.ts b/apps/dokploy/__test__/deploy/github.test.ts index 18d7619ab..03805b08d 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 95% rename from apps/dokploy/__test__/drop/drop.test.test.ts rename to apps/dokploy/__test__/drop/drop.test.ts index 8fda40e51..b597b3aa4 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,7 +25,9 @@ if (typeof window === "undefined") { } const baseApp: ApplicationNested = { + railpackVersion: "0.2.2", applicationId: "", + previewLabels: [], herokuVersion: "", giteaBranch: "", giteaBuildPath: "", @@ -54,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: "/", @@ -90,6 +100,7 @@ const baseApp: ApplicationNested = { dockerfile: null, dockerImage: null, dropBuildPath: null, + environmentId: "", enabled: null, env: null, healthCheckSwarm: null, @@ -104,7 +115,6 @@ const baseApp: ApplicationNested = { password: null, placementSwarm: null, ports: [], - projectId: "", publishDirectory: null, isStaticSpa: null, redirects: [], @@ -142,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 000000000..95d46dcc0 --- /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 4a8448aa9..5e231a5cc 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 997bd9ec5..53ca8d777 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__/traefik/traefik.test.ts b/apps/dokploy/__test__/traefik/traefik.test.ts index c1517d530..5be96e473 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: "", @@ -36,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: "", @@ -85,7 +94,6 @@ const baseApp: ApplicationNested = { password: null, placementSwarm: null, ports: [], - projectId: "", publishDirectory: null, isStaticSpa: null, redirects: [], 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 b8a272e15..9e10f43ec 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({ @@ -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 { mutateAsync, isError, error, isLoading } = - api.application.update.useMutation(); + 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 } = 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,7 +292,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { Swarm Settings - + Swarm Settings @@ -278,10 +300,10 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { {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 diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx index 57f851c9e..a3bc8079a 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx @@ -1,3 +1,10 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { Server } from "lucide-react"; +import Link from "next/link"; +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 { @@ -26,43 +33,57 @@ import { SelectValue, } from "@/components/ui/select"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Server } from "lucide-react"; -import Link from "next/link"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; import { AddSwarmSettings } from "./modify-swarm-settings"; interface Props { - applicationId: string; + id: string; + type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; } const AddRedirectchema = z.object({ replicas: z.number().min(1, "Replicas must be at least 1"), - registryId: z.string(), + registryId: z.string().optional(), }); type AddCommand = z.infer; -export const ShowClusterSettings = ({ applicationId }: Props) => { - const { data } = api.application.one.useQuery( - { - applicationId, - }, - { enabled: !!applicationId }, - ); - +export const ShowClusterSettings = ({ 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 { data: registries } = api.registry.all.useQuery(); - const utils = api.useUtils(); + 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, isLoading } = api.application.update.useMutation(); + const { mutateAsync, isLoading } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); const form = useForm({ defaultValues: { - registryId: data?.registryId || "", + ...(type === "application" && data && "registryId" in data + ? { + registryId: data?.registryId || "", + } + : {}), replicas: data?.replicas || 1, }, resolver: zodResolver(AddRedirectchema), @@ -71,7 +92,11 @@ export const ShowClusterSettings = ({ applicationId }: Props) => { useEffect(() => { if (data?.command) { form.reset({ - registryId: data?.registryId || "", + ...(type === "application" && data && "registryId" in data + ? { + registryId: data?.registryId || "", + } + : {}), replicas: data?.replicas || 1, }); } @@ -79,18 +104,25 @@ export const ShowClusterSettings = ({ applicationId }: Props) => { const onSubmit = async (data: AddCommand) => { await mutateAsync({ - applicationId, - registryId: - data?.registryId === "none" || !data?.registryId - ? null - : data?.registryId, + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + ...(type === "application" + ? { + registryId: + data?.registryId === "none" || !data?.registryId + ? null + : data?.registryId, + } + : {}), replicas: data?.replicas, }) .then(async () => { toast.success("Command Updated"); - await utils.application.one.invalidate({ - applicationId, - }); + await refetch(); }) .catch(() => { toast.error("Error updating the command"); @@ -103,10 +135,10 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
Cluster Settings - Add the registry and the replicas of the application + Modify swarm settings for the service.
- + @@ -144,58 +176,62 @@ export const ShowClusterSettings = ({ 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 50e36ad76..1bf69394a 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 29033f6b6..17d033cf2 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(), 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 81c1f32c5..568792461 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,12 +32,6 @@ 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), @@ -80,6 +80,11 @@ export const HandlePorts = ({ resolver: zodResolver(AddPortSchema), }); + const publishMode = useWatch({ + control: form.control, + name: "publishMode", + }); + useEffect(() => { form.reset({ publishedPort: data?.publishedPort ?? 0, @@ -253,6 +258,16 @@ export const HandlePorts = ({ + {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. + + )} + + + + )} {refreshToken && (
@@ -104,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") && ( diff --git a/apps/dokploy/components/dashboard/application/domains/dns-helper-modal.tsx b/apps/dokploy/components/dashboard/application/domains/dns-helper-modal.tsx index c67c2fbfc..768ece858 100644 --- a/apps/dokploy/components/dashboard/application/domains/dns-helper-modal.tsx +++ b/apps/dokploy/components/dashboard/application/domains/dns-helper-modal.tsx @@ -1,3 +1,5 @@ +import { Copy, HelpCircle, Server } from "lucide-react"; +import { toast } from "sonner"; import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; import { @@ -8,8 +10,6 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; -import { Copy, HelpCircle, Server } from "lucide-react"; -import { toast } from "sonner"; interface Props { domain: { diff --git a/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx b/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx index 9069542d9..9d7a074f9 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"; @@ -123,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( @@ -325,46 +325,126 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => { Service Name
- + ) : ( + + + + )} + {!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]" >

- 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 + {isManualInput + ? "Switch to service selection" + : "Enter service name manually"}

diff --git a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx index 7bb58dfbe..1fd3d82e9 100644 --- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx @@ -1,3 +1,18 @@ +import { + CheckCircle2, + ExternalLink, + GlobeIcon, + InfoIcon, + Loader2, + PenBoxIcon, + RefreshCw, + Server, + Trash2, + XCircle, +} from "lucide-react"; +import Link from "next/link"; +import { useState } from "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,21 +30,6 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { api } from "@/utils/api"; -import { - CheckCircle2, - ExternalLink, - GlobeIcon, - InfoIcon, - Loader2, - PenBoxIcon, - RefreshCw, - Server, - Trash2, - XCircle, -} from "lucide-react"; -import Link from "next/link"; -import { useState } from "react"; -import { toast } from "sonner"; import { DnsHelperModal } from "./dns-helper-modal"; import { AddDomain } from "./handle-domain"; diff --git a/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx b/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx index 8a78c2745..4a5d0270b 100644 --- a/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx +++ b/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx @@ -1,3 +1,9 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { EyeIcon, EyeOffIcon } from "lucide-react"; +import { type CSSProperties, useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; import { CodeEditor } from "@/components/shared/code-editor"; import { Button } from "@/components/ui/button"; import { @@ -16,12 +22,6 @@ import { } from "@/components/ui/form"; import { Toggle } from "@/components/ui/toggle"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { EyeIcon, EyeOffIcon } from "lucide-react"; -import { type CSSProperties, useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; import type { ServiceType } from "../advanced/show-resources"; const addEnvironmentSchema = z.object({ diff --git a/apps/dokploy/components/dashboard/application/environment/show.tsx b/apps/dokploy/components/dashboard/application/environment/show.tsx index 6f504959c..78edb1aaa 100644 --- a/apps/dokploy/components/dashboard/application/environment/show.tsx +++ b/apps/dokploy/components/dashboard/application/environment/show.tsx @@ -1,13 +1,13 @@ -import { Button } from "@/components/ui/button"; -import { Card } from "@/components/ui/card"; -import { Form } from "@/components/ui/form"; -import { Secrets } from "@/components/ui/secrets"; -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 { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Form } from "@/components/ui/form"; +import { Secrets } from "@/components/ui/secrets"; +import { api } from "@/utils/api"; const addEnvironmentSchema = z.object({ env: z.string(), diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx index befc85957..6f6db5dd1 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx @@ -1,3 +1,10 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { CheckIcon, ChevronsUpDown, X } from "lucide-react"; +import Link from "next/link"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; import { BitbucketIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; import { Badge } from "@/components/ui/badge"; @@ -40,13 +47,6 @@ import { } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { CheckIcon, ChevronsUpDown, X } from "lucide-react"; -import Link from "next/link"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const BitbucketProviderSchema = z.object({ buildPath: z.string().min(1, "Path is required").default("/"), diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-docker-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-docker-provider.tsx index 72b2578c5..fcdcf0a93 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-docker-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-docker-provider.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 { Form, @@ -9,11 +14,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"; const DockerProviderSchema = z.object({ dockerImage: z.string().min(1, { diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx index 3732860d4..00e18c2ab 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx @@ -1,3 +1,8 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { TrashIcon } from "lucide-react"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Dropzone } from "@/components/ui/dropzone"; import { @@ -11,11 +16,6 @@ import { import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; import { type UploadFile, uploadFileSchema } from "@/utils/schema"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { TrashIcon } from "lucide-react"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; interface Props { applicationId: string; diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx index f3e8116e6..61690e740 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx @@ -1,3 +1,13 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { KeyRoundIcon, LockIcon, X } from "lucide-react"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { GitIcon } from "@/components/icons/data-tools-icons"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Form, @@ -25,17 +35,6 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { KeyRoundIcon, LockIcon, X } from "lucide-react"; -import Link from "next/link"; -import { useRouter } from "next/router"; - -import { GitIcon } from "@/components/icons/data-tools-icons"; -import { Badge } from "@/components/ui/badge"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const GitProviderSchema = z.object({ buildPath: z.string().min(1, "Path is required").default("/"), diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx index 55fbfebda..2198f4a97 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx @@ -1,3 +1,10 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react"; +import Link from "next/link"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; import { GiteaIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; import { Badge } from "@/components/ui/badge"; @@ -40,13 +47,6 @@ import { } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react"; -import Link from "next/link"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; interface GiteaRepository { name: string; diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx index c76b9ae58..9a4b92ce1 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx @@ -1,3 +1,10 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react"; +import Link from "next/link"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; import { GithubIcon } from "@/components/icons/data-tools-icons"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -39,13 +46,6 @@ import { } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react"; -import Link from "next/link"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const GithubProviderSchema = z.object({ buildPath: z.string().min(1, "Path is required").default("/"), diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx index 2995e45f3..cb7209f8a 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx @@ -1,3 +1,10 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react"; +import Link from "next/link"; +import { useEffect, useMemo } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; import { GitlabIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; import { Badge } from "@/components/ui/badge"; @@ -40,13 +47,6 @@ import { } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react"; -import Link from "next/link"; -import { useEffect, useMemo } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; const GitlabProviderSchema = z.object({ buildPath: z.string().min(1, "Path is required").default("/"), diff --git a/apps/dokploy/components/dashboard/application/general/generic/show.tsx b/apps/dokploy/components/dashboard/application/general/generic/show.tsx index 786c79e5c..a60db800c 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/show.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/show.tsx @@ -1,3 +1,7 @@ +import { GitBranch, Loader2, UploadCloud } from "lucide-react"; +import Link from "next/link"; +import { useState } from "react"; +import { toast } from "sonner"; import { SaveDockerProvider } from "@/components/dashboard/application/general/generic/save-docker-provider"; import { SaveGitProvider } from "@/components/dashboard/application/general/generic/save-git-provider"; import { SaveGiteaProvider } from "@/components/dashboard/application/general/generic/save-gitea-provider"; @@ -5,18 +9,14 @@ import { SaveGithubProvider } from "@/components/dashboard/application/general/g import { BitbucketIcon, DockerIcon, - GitIcon, GiteaIcon, GithubIcon, + GitIcon, GitlabIcon, } from "@/components/icons/data-tools-icons"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { api } from "@/utils/api"; -import { GitBranch, Loader2, UploadCloud } from "lucide-react"; -import Link from "next/link"; -import { useState } from "react"; -import { toast } from "sonner"; import { SaveBitbucketProvider } from "./save-bitbucket-provider"; import { SaveDragNDrop } from "./save-drag-n-drop"; import { SaveGitlabProvider } from "./save-gitlab-provider"; diff --git a/apps/dokploy/components/dashboard/application/general/generic/unauthorized-git-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/unauthorized-git-provider.tsx index 4dbdf7a69..de3fbff06 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/unauthorized-git-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/unauthorized-git-provider.tsx @@ -1,8 +1,9 @@ +import { AlertCircle, GitBranch, Unlink } from "lucide-react"; import { BitbucketIcon, - GitIcon, GiteaIcon, GithubIcon, + GitIcon, GitlabIcon, } from "@/components/icons/data-tools-icons"; import { DialogAction } from "@/components/shared/dialog-action"; @@ -10,7 +11,6 @@ import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import type { RouterOutputs } from "@/utils/api"; -import { AlertCircle, GitBranch, Unlink } from "lucide-react"; interface Props { service: diff --git a/apps/dokploy/components/dashboard/application/general/show.tsx b/apps/dokploy/components/dashboard/application/general/show.tsx index c917d7ab7..5387659ad 100644 --- a/apps/dokploy/components/dashboard/application/general/show.tsx +++ b/apps/dokploy/components/dashboard/application/general/show.tsx @@ -1,3 +1,14 @@ +import * as TooltipPrimitive from "@radix-ui/react-tooltip"; +import { + Ban, + CheckCircle2, + Hammer, + RefreshCcw, + Rocket, + Terminal, +} from "lucide-react"; +import { useRouter } from "next/router"; +import { toast } from "sonner"; import { ShowBuildChooseForm } from "@/components/dashboard/application/build/show"; import { ShowProviderForm } from "@/components/dashboard/application/general/generic/show"; import { DialogAction } from "@/components/shared/dialog-action"; @@ -11,18 +22,8 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { api } from "@/utils/api"; -import * as TooltipPrimitive from "@radix-ui/react-tooltip"; -import { - Ban, - CheckCircle2, - Hammer, - RefreshCcw, - Rocket, - Terminal, -} from "lucide-react"; -import { useRouter } from "next/router"; -import { toast } from "sonner"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; + interface Props { applicationId: string; } @@ -68,7 +69,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => { 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 a73b99d25..e5dff075e 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 bb6f0e0a7..eac4559f1 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; 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 bf93af718..d93bbd1c8 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 a0f6ae0e4..16c916d93 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,6 +49,7 @@ 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"]), @@ -81,6 +89,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => { wildcardDomain: "*.traefik.me", port: 3000, previewLimit: 3, + previewLabels: [], previewHttps: false, previewPath: "/", previewCertificateType: "none", @@ -102,6 +111,7 @@ 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 || "/", @@ -119,6 +129,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => { previewBuildArgs: formData.buildArgs, previewWildcard: formData.wildcardDomain, previewPort: formData.port, + previewLabels: formData.previewLabels, applicationId, previewLimit: formData.previewLimit, previewHttps: formData.previewHttps, @@ -200,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 = ""; + } + } + }} + /> + + +
+ +
+ )} + /> { return ( -
+
Scheduled Tasks @@ -91,15 +91,15 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => { return (
-
+
-
-

+
+

{schedule.name}

{ {schedule.enabled ? "Enabled" : "Disabled"}
-
+
{
-
+
{ })}
) : ( -
+

No scheduled tasks diff --git a/apps/dokploy/components/dashboard/application/update-application.tsx b/apps/dokploy/components/dashboard/application/update-application.tsx index 4d4190fa2..754074d75 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, { 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 index c66b05850..f00b91a9d 100644 --- a/apps/dokploy/components/dashboard/application/volume-backups/handle-volume-backups.tsx +++ b/apps/dokploy/components/dashboard/application/volume-backups/handle-volume-backups.tsx @@ -1,3 +1,15 @@ +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 { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; import { @@ -34,18 +46,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"; import { commonCronExpressions } from "../schedules/handle-schedules"; @@ -55,7 +55,12 @@ const formSchema = z 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().optional(), + 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([ @@ -108,6 +113,7 @@ export const HandleVolumeBackups = ({ }: Props) => { const [isOpen, setIsOpen] = useState(false); const [cacheType, setCacheType] = useState("cache"); + const [keepLatestCountInput, setKeepLatestCountInput] = useState(""); const utils = api.useUtils(); const form = useForm>({ @@ -117,7 +123,7 @@ export const HandleVolumeBackups = ({ cronExpression: "", volumeName: "", prefix: "", - // keepLatestCount: undefined, + keepLatestCount: undefined, turnOff: false, enabled: true, serviceName: "", @@ -173,13 +179,19 @@ export const HandleVolumeBackups = ({ cronExpression: volumeBackup.cronExpression, volumeName: volumeBackup.volumeName || "", prefix: volumeBackup.prefix, - // keepLatestCount: volumeBackup.keepLatestCount || undefined, + 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]); @@ -190,8 +202,12 @@ export const HandleVolumeBackups = ({ 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, @@ -257,9 +273,8 @@ export const HandleVolumeBackups = ({ @@ -600,29 +615,38 @@ export const HandleVolumeBackups = ({ )} /> - {/* ( - Keep Latest Count + Keep Latest Backups - field.onChange(Number(e.target.value) || undefined) - } + type="number" + min={1} + autoComplete="off" + placeholder="Leave empty to keep all" + value={keepLatestCountInput} + onChange={(e) => { + const raw = e.target.value; + setKeepLatestCountInput(raw); + if (raw === "") { + field.onChange(undefined); + } else if (/^\d+$/.test(raw)) { + field.onChange(Number(raw)); + } + }} /> - Number of backup files to keep (optional) + How many recent backups to keep. Empty means no cleanup. )} - /> */} + /> ; + +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 65689afd1..e75aad5e5 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 ( @@ -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 5ac67e0c8..2c488aefe 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 677762b00..fac6c2a34 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; @@ -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 71752525c..4199363d8 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 d166f933f..4c004918b 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 571190549..a4551f415 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 f9c38a6bc..7564988e2 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, { diff --git a/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx b/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx index 4c5bbe628..a0449a155 100644 --- a/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx @@ -1,3 +1,17 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { + CheckIcon, + ChevronsUpDown, + DatabaseZap, + Info, + 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,19 +62,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, - 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"; type CacheType = "cache" | "fetch"; diff --git a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx index a173f85ad..6a0fb030a 100644 --- a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx @@ -1,3 +1,18 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import copy from "copy-to-clipboard"; +import { debounce } from "lodash"; +import { + CheckIcon, + ChevronsUpDown, + Copy, + DatabaseZap, + RefreshCw, + RotateCcw, +} from "lucide-react"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; import { DrawerLogs } from "@/components/shared/drawer-logs"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -47,21 +62,6 @@ import { } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import copy from "copy-to-clipboard"; -import { debounce } from "lodash"; -import { - CheckIcon, - ChevronsUpDown, - Copy, - DatabaseZap, - RefreshCw, - RotateCcw, -} from "lucide-react"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; import type { ServiceType } from "../../application/advanced/show-resources"; import { type LogLine, parseLogs } from "../../docker/logs/utils"; diff --git a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx index 28ee68a9c..55a09b25f 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/logs/docker-logs-id.tsx b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx index 611af355e..54db7945b 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -1,13 +1,14 @@ +import { Download as DownloadIcon, Loader2, Pause, Play } from "lucide-react"; +import React, { useEffect, useRef } from "react"; +import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; -import { Download as DownloadIcon, Loader2 } from "lucide-react"; -import React, { useEffect, useRef } from "react"; import { LineCountFilter } from "./line-count-filter"; import { SinceLogsFilter, type TimeFilter } from "./since-logs-filter"; import { StatusLogsFilter } from "./status-logs-filter"; import { TerminalLine } from "./terminal-line"; -import { type LogLine, getLogType, parseLogs } from "./utils"; +import { getLogType, type LogLine, parseLogs } from "./utils"; interface Props { containerId: string; @@ -61,6 +62,9 @@ export const DockerLogsId: React.FC = ({ 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( 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 669369348..0399e2c67 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( 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 44f2cdfc3..986a19059 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 3ef11517a..22c1ed648 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 48ec4557b..5b929f3b6 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 1cf0200f2..74fe6819e 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 024b00618..52398aabe 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 97d9f16e8..62c1347e4 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), diff --git a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx index bf14680a4..ad34d69ce 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 fb5fe8f5c..8c848a0dc 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 c9272f293..94a5c72a6 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 8a9f55c90..7804e9add 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 c00af42be..8745db286 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 2f8bab77b..8e996846f 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 9d29d1ac4..62486e015 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, { 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 75772bfdf..d30061db5 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 fdc28adc3..23fbe51d3 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 48dbcf4d7..e78abddbd 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, { 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 82a1ff3d5..34a3913a7 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 cd6b7dfde..5e2414cea 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 84510154c..246ae296d 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 117fae388..b28c4d9b6 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 12af6b91d..32e30f62a 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 445e03e12..76b010c7c 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 4da864285..ff5e85843 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 d51e89687..f962e2ae3 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 4ca461c21..026043806 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 c9cefa4c3..db087afa0 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"; 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 8c9602ee2..efa84ffc4 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 f4079c46d..1981dace3 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 b84af0952..bbb522fdc 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 492abc9e0..af0dacc1d 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"; 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 73f99b7d0..dfaa36f6b 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 590127fa7..045a717b7 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 9b1296478..353523aa0 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, { diff --git a/apps/dokploy/components/dashboard/organization/handle-organization.tsx b/apps/dokploy/components/dashboard/organization/handle-organization.tsx index 394f3d018..c676e0233 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) => { 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 40e84844f..febaa8644 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 444fa0cee..46b3772a0 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 fec51b5a2..de520053d 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 2695953cd..d4485862e 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, { diff --git a/apps/dokploy/components/dashboard/project/add-ai-assistant.tsx b/apps/dokploy/components/dashboard/project/add-ai-assistant.tsx index 2bb47618e..88fd1d111 100644 --- a/apps/dokploy/components/dashboard/project/add-ai-assistant.tsx +++ b/apps/dokploy/components/dashboard/project/add-ai-assistant.tsx @@ -1,10 +1,10 @@ import { TemplateGenerator } from "@/components/dashboard/project/ai/template-generator"; interface Props { - projectId: string; + environmentId: string; projectName?: string; } -export const AddAiAssistant = ({ projectId }: Props) => { - return ; +export const AddAiAssistant = ({ environmentId }: Props) => { + return ; }; diff --git a/apps/dokploy/components/dashboard/project/add-application.tsx b/apps/dokploy/components/dashboard/project/add-application.tsx index 137f75a51..079701eb8 100644 --- a/apps/dokploy/components/dashboard/project/add-application.tsx +++ b/apps/dokploy/components/dashboard/project/add-application.tsx @@ -64,11 +64,11 @@ const AddTemplateSchema = z.object({ type AddTemplate = z.infer; interface Props { - projectId: string; + environmentId: string; projectName?: string; } -export const AddApplication = ({ projectId, projectName }: Props) => { +export const AddApplication = ({ environmentId, projectName }: Props) => { const utils = api.useUtils(); const { data: isCloud } = api.settings.isCloud.useQuery(); const [visible, setVisible] = useState(false); @@ -76,6 +76,10 @@ export const AddApplication = ({ projectId, projectName }: Props) => { 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(); @@ -94,15 +98,15 @@ 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(() => { @@ -157,7 +161,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => { )} /> - {hasServers && ( + {shouldShowServerDropdown && ( { diff --git a/apps/dokploy/components/dashboard/project/add-compose.tsx b/apps/dokploy/components/dashboard/project/add-compose.tsx index c32e55c16..a187104ec 100644 --- a/apps/dokploy/components/dashboard/project/add-compose.tsx +++ b/apps/dokploy/components/dashboard/project/add-compose.tsx @@ -65,11 +65,11 @@ const AddComposeSchema = z.object({ type AddCompose = z.infer; interface Props { - projectId: string; + environmentId: string; projectName?: string; } -export const AddCompose = ({ projectId, projectName }: Props) => { +export const AddCompose = ({ environmentId, projectName }: Props) => { const utils = api.useUtils(); const [visible, setVisible] = useState(false); const slug = slugify(projectName); @@ -78,7 +78,14 @@ 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: { @@ -98,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(() => { @@ -165,7 +173,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => { )} />
- {hasServers && ( + {shouldShowServerDropdown && ( { diff --git a/apps/dokploy/components/dashboard/project/add-database.tsx b/apps/dokploy/components/dashboard/project/add-database.tsx index 6b07baa81..064e93544 100644 --- a/apps/dokploy/components/dashboard/project/add-database.tsx +++ b/apps/dokploy/components/dashboard/project/add-database.tsx @@ -83,7 +83,12 @@ const baseDatabaseSchema = z.object({ message: "App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'", }), - databasePassword: z.string(), + databasePassword: z + .string() + .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { + message: + "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", + }), dockerImage: z.string(), description: z.string().nullable(), serverId: z.string().nullable(), @@ -112,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"), }) @@ -121,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"), }) @@ -154,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(); @@ -169,7 +187,14 @@ 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: { @@ -203,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, }; @@ -216,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({ @@ -224,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({ @@ -251,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 || "", }); } @@ -271,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(() => { @@ -382,7 +407,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { )} /> - {hasServers && ( + {shouldShowServerDropdown && ( { Select a Server setName(e.target.value)} + placeholder="Environment name" + /> +
+
+ +