diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..58825f900 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,21 @@ +## 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) + +Close automatically the related issues using the keywords: `closes #ISSUE_NUMBER`, `fixes #ISSUE_NUMBER`, `resolves #ISSUE_NUMBER` + +Example: `closes #123` + +## Screenshots (if applicable) + +If you include a video or screenshot, would be awesome so we can see the changes in action. \ No newline at end of file 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/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..fb2ee82a5 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.

@@ -13,7 +13,7 @@ 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 +43,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 +95,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 +106,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/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 98% rename from apps/dokploy/__test__/drop/drop.test.test.ts rename to apps/dokploy/__test__/drop/drop.test.ts index 8fda40e51..301278dc3 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,6 +25,7 @@ if (typeof window === "undefined") { } const baseApp: ApplicationNested = { + railpackVersion: "0.2.2", applicationId: "", herokuVersion: "", giteaBranch: "", @@ -142,7 +143,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__/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..8d9f78aba 100644 --- a/apps/dokploy/__test__/traefik/traefik.test.ts +++ b/apps/dokploy/__test__/traefik/traefik.test.ts @@ -1,10 +1,9 @@ -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: "", herokuVersion: "", 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/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. + + )} + + + +

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

+ Resources that will be isolated: +

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

+ Generating compose preview... +

+
+ ) : ( +
+													
+												
+ )} +
+
+
+
+
+ +
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx b/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx index 41e40efbe..c2db472d2 100644 --- a/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx +++ b/apps/dokploy/components/dashboard/compose/general/compose-file-editor.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 { CodeEditor } from "@/components/shared/code-editor"; import { Button } from "@/components/ui/button"; import { @@ -8,13 +13,7 @@ import { FormMessage, } from "@/components/ui/form"; 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 { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config"; -import { ShowUtilities } from "./show-utilities"; interface Props { composeId: string; @@ -142,9 +141,7 @@ services:
-
- -
+
-
- -
- -
-							
-						
-
- - - - ); -}; 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/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) + + )} + +
+
+ )}
{ const { data: auth } = api.user.get.useQuery(); const { mutateAsync } = api.project.remove.useMutation(); const [searchQuery, setSearchQuery] = useState(""); + const [sortBy, setSortBy] = useState(() => { + if (typeof window !== "undefined") { + return localStorage.getItem("projectsSort") || "createdAt-desc"; + } + return "createdAt-desc"; + }); + + useEffect(() => { + localStorage.setItem("projectsSort", sortBy); + }, [sortBy]); const filteredProjects = useMemo(() => { if (!data) return []; - return data.filter( + + // First filter by search query + const filtered = data.filter( (project) => project.name.toLowerCase().includes(searchQuery.toLowerCase()) || project.description?.toLowerCase().includes(searchQuery.toLowerCase()), ); - }, [data, searchQuery]); + + // Then sort the filtered results + const [field, direction] = sortBy.split("-"); + return [...filtered].sort((a, b) => { + let comparison = 0; + switch (field) { + case "name": + comparison = a.name.localeCompare(b.name); + break; + case "createdAt": + comparison = + new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); + break; + case "services": { + const aTotalServices = + a.mariadb.length + + a.mongo.length + + a.mysql.length + + a.postgres.length + + a.redis.length + + a.applications.length + + a.compose.length; + const bTotalServices = + b.mariadb.length + + b.mongo.length + + b.mysql.length + + b.postgres.length + + b.redis.length + + b.applications.length + + b.compose.length; + comparison = aTotalServices - bTotalServices; + break; + } + default: + comparison = 0; + } + return direction === "asc" ? comparison : -comparison; + }); + }, [data, searchQuery, sortBy]); return ( <> @@ -98,14 +156,40 @@ export const ShowProjects = () => {
) : ( <> -
- setSearchQuery(e.target.value)} - className="pr-10" - /> - +
+
+ setSearchQuery(e.target.value)} + className="pr-10" + /> + +
+
+ + +
{filteredProjects?.length === 0 && (
diff --git a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx index 49fdfd2dd..89014601e 100644 --- a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx @@ -1,3 +1,4 @@ +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; import { @@ -19,7 +20,7 @@ import { import { Input } from "@/components/ui/input"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Switch } from "@/components/ui/switch"; -import { generateSHA256Hash } from "@/lib/utils"; +import { generateSHA256Hash, getFallbackAvatarInitials } from "@/lib/utils"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { Loader2, User } from "lucide-react"; @@ -257,6 +258,24 @@ export const ProfileForm = () => { value={field.value} className="flex flex-row flex-wrap gap-2 max-xl:justify-center" > + + + + + + + + + {getFallbackAvatarInitials( + data?.user?.name, + )} + + + + {availableAvatars.map((image) => ( diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx index c0c45e147..38039cc6b 100644 --- a/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx @@ -1,5 +1,6 @@ +import { useTranslation } from "next-i18next"; +import { toast } from "sonner"; import { Button } from "@/components/ui/button"; - import { DropdownMenu, DropdownMenuContent, @@ -10,8 +11,6 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { api } from "@/utils/api"; -import { useTranslation } from "next-i18next"; -import { toast } from "sonner"; import { EditTraefikEnv } from "../../web-server/edit-traefik-env"; import { ManageTraefikPorts } from "../../web-server/manage-traefik-ports"; import { ShowModalLogs } from "../../web-server/show-modal-logs"; diff --git a/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx b/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx index a5cfb6308..282f1fddd 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx @@ -1,3 +1,11 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { ArrowRightLeft, Plus, Trash2 } from "lucide-react"; +import { useTranslation } from "next-i18next"; +import type React from "react"; +import { useEffect, useState } from "react"; +import { useFieldArray, 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 { Card, CardContent } from "@/components/ui/card"; @@ -19,15 +27,15 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { ArrowRightLeft, Plus, Trash2 } from "lucide-react"; -import { useTranslation } from "next-i18next"; -import type React from "react"; -import { useEffect, useState } from "react"; -import { useFieldArray, useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; interface Props { children: React.ReactNode; @@ -37,6 +45,7 @@ interface Props { const PortSchema = z.object({ targetPort: z.number().min(1, "Target port is required"), publishedPort: z.number().min(1, "Published port is required"), + protocol: z.enum(["tcp", "udp", "sctp"]), }); const TraefikPortsSchema = z.object({ @@ -75,12 +84,17 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => { useEffect(() => { if (currentPorts) { - form.reset({ ports: currentPorts }); + form.reset({ + ports: currentPorts.map((port) => ({ + ...port, + protocol: port.protocol as "tcp" | "udp" | "sctp", + })), + }); } }, [currentPorts, form]); const handleAddPort = () => { - append({ targetPort: 0, publishedPort: 0 }); + append({ targetPort: 0, publishedPort: 0, protocol: "tcp" }); }; const onSubmit = async (data: TraefikPortsForm) => { @@ -96,7 +110,9 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => { return ( <> -
setOpen(true)}>{children}
+ @@ -143,8 +159,8 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
{fields.map((field, index) => ( - - + + { ); }} value={field.value || ""} - className="w-full dark:bg-black" placeholder="e.g. 8080" /> @@ -200,7 +215,6 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => { ); }} value={field.value || ""} - className="w-full dark:bg-black" placeholder="e.g. 80" /> @@ -208,6 +222,42 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => { )} /> + ( + + + Protocol + + + + + + + )} + />