diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 000000000..4ffcfaed7
--- /dev/null
+++ b/.devcontainer/Dockerfile
@@ -0,0 +1,21 @@
+# Dockerfile for DevContainer
+FROM node:24.4.0-bullseye-slim
+
+# Install essential packages
+RUN apt-get update && apt-get install -y \
+ curl \
+ bash \
+ git \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/*
+
+# Set up PNPM
+ENV PNPM_HOME="/pnpm"
+ENV PATH="$PNPM_HOME:$PATH"
+RUN corepack enable && corepack prepare pnpm@10.22.0 --activate
+
+# Create workspace directory
+WORKDIR /workspaces/dokploy
+
+# Set up user permissions
+USER node
\ No newline at end of file
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 000000000..eafddd06d
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,53 @@
+{
+ "name": "Dokploy development container",
+ "build": {
+ "dockerfile": "Dockerfile",
+ "context": ".."
+ },
+ "features": {
+ "ghcr.io/devcontainers/features/docker-in-docker:2": {
+ "moby": true,
+ "version": "latest"
+ },
+ "ghcr.io/devcontainers/features/git:1": {
+ "ppa": true,
+ "version": "latest"
+ },
+ "ghcr.io/devcontainers/features/go:1": {
+ "version": "1.20"
+ }
+ },
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "ms-vscode.vscode-typescript-next",
+ "bradlc.vscode-tailwindcss",
+ "ms-vscode.vscode-json",
+ "biomejs.biome",
+ "golang.go",
+ "redhat.vscode-xml",
+ "github.vscode-github-actions",
+ "github.copilot",
+ "github.copilot-chat"
+ ]
+ }
+ },
+ "forwardPorts": [3000, 5432, 6379],
+ "portsAttributes": {
+ "3000": {
+ "label": "Dokploy App",
+ "onAutoForward": "notify"
+ },
+ "5432": {
+ "label": "PostgreSQL",
+ "onAutoForward": "silent"
+ },
+ "6379": {
+ "label": "Redis",
+ "onAutoForward": "silent"
+ }
+ },
+ "remoteUser": "node",
+ "workspaceFolder": "/workspaces/dokploy",
+ "runArgs": ["--name", "dokploy-devcontainer"]
+}
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index d45c3dac0..e210811b0 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -8,7 +8,7 @@ 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.
+- [ ] You have tested this PR in your local instance. If you have not tested it yet, please do so before submitting. This helps avoid wasting maintainers' time reviewing code that has not been verified by you.
## Issues related (if applicable)
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 3ed957b72..321fb2029 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -13,6 +13,17 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
+ - name: Set tag and version
+ id: meta-cloud
+ run: |
+ VERSION=$(jq -r .version apps/dokploy/package.json)
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+ if [ "${{ github.ref }}" = "refs/heads/main" ]; then
+ echo "tags=siumauricio/cloud:latest,siumauricio/cloud:${VERSION}" >> $GITHUB_OUTPUT
+ else
+ echo "tags=siumauricio/cloud:canary" >> $GITHUB_OUTPUT
+ fi
+
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
@@ -25,8 +36,7 @@ jobs:
context: .
file: ./Dockerfile.cloud
push: true
- tags: |
- siumauricio/cloud:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
+ tags: ${{ steps.meta-cloud.outputs.tags }}
platforms: linux/amd64
build-args: |
NEXT_PUBLIC_UMAMI_HOST=${{ secrets.NEXT_PUBLIC_UMAMI_HOST }}
@@ -40,6 +50,16 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
+ - name: Set tag and version
+ id: meta-schedule
+ run: |
+ VERSION=$(jq -r .version apps/dokploy/package.json)
+ if [ "${{ github.ref }}" = "refs/heads/main" ]; then
+ echo "tags=siumauricio/schedule:latest,siumauricio/schedule:${VERSION}" >> $GITHUB_OUTPUT
+ else
+ echo "tags=siumauricio/schedule:canary" >> $GITHUB_OUTPUT
+ fi
+
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
@@ -52,8 +72,7 @@ jobs:
context: .
file: ./Dockerfile.schedule
push: true
- tags: |
- siumauricio/schedule:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
+ tags: ${{ steps.meta-schedule.outputs.tags }}
platforms: linux/amd64
build-and-push-server-image:
@@ -63,6 +82,16 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
+ - name: Set tag and version
+ id: meta-server
+ run: |
+ VERSION=$(jq -r .version apps/dokploy/package.json)
+ if [ "${{ github.ref }}" = "refs/heads/main" ]; then
+ echo "tags=siumauricio/server:latest,siumauricio/server:${VERSION}" >> $GITHUB_OUTPUT
+ else
+ echo "tags=siumauricio/server:canary" >> $GITHUB_OUTPUT
+ fi
+
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
@@ -75,6 +104,5 @@ jobs:
context: .
file: ./Dockerfile.server
push: true
- tags: |
- siumauricio/server:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
+ tags: ${{ steps.meta-server.outputs.tags }}
platforms: linux/amd64
diff --git a/.github/workflows/pr-quality.yml b/.github/workflows/pr-quality.yml
new file mode 100644
index 000000000..3554babb2
--- /dev/null
+++ b/.github/workflows/pr-quality.yml
@@ -0,0 +1,22 @@
+
+name: PR Quality
+
+permissions:
+ contents: read
+ issues: read
+ pull-requests: write
+
+on:
+ pull_request_target:
+ types: [opened, reopened]
+
+jobs:
+ anti-slop:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: peakoss/anti-slop@v0
+ with:
+ max-failures: 4
+ blocked-commit-authors: "claude,copilot"
+ require-description: true
+ min-account-age: 5
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index bfdc8c48b..2ad24fc0c 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -18,7 +18,7 @@ jobs:
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
- node-version: 20.16.0
+ node-version: 24.4.0
cache: "pnpm"
- name: Install Nixpacks
diff --git a/.github/workflows/sync-openapi-docs.yml b/.github/workflows/sync-openapi-docs.yml
index ddc51355a..549af945b 100644
--- a/.github/workflows/sync-openapi-docs.yml
+++ b/.github/workflows/sync-openapi-docs.yml
@@ -24,7 +24,7 @@ jobs:
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
- node-version: 20.16.0
+ node-version: 24.4.0
cache: "pnpm"
- name: Install dependencies
diff --git a/.gitignore b/.gitignore
index ab2fe76c6..d531bab01 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,7 +43,4 @@ yarn-error.log*
*.pem
-.db
-
-# Development environment
-.devcontainer
\ No newline at end of file
+.db
\ No newline at end of file
diff --git a/.nvmrc b/.nvmrc
index 593cb75bc..84e5de6ef 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-20.16.0
\ No newline at end of file
+24.4.0
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4c1f832db..ad37899e6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,7 +2,7 @@
Hey, thanks for your interest in contributing to Dokploy! We appreciate your help and taking your time to contribute.
-Before you start, please first discuss the feature/bug you want to add with the owners and comunity via github issues.
+Before you start, please first discuss the feature/bug you want to add with the owners and community via github issues.
We have a few guidelines to follow when contributing to this project:
@@ -11,6 +11,7 @@ We have a few guidelines to follow when contributing to this project:
- [Development](#development)
- [Build](#build)
- [Pull Request](#pull-request)
+- [Important Considerations](#important-considerations-for-pull-requests)
## Commit Convention
@@ -52,7 +53,7 @@ feat: add new feature
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
-We use Node v20.16.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.16.0 && nvm use` in the root directory.
+We use Node v24.4.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 24.4.0 && nvm use` in the root directory.
```bash
git clone https://github.com/dokploy/dokploy.git
@@ -162,11 +163,13 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.39.1/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:**
+### Important Considerations for Pull Requests
+- **Testing is Mandatory:** All Pull Requests **must be tested** by the PR author before submission. You must verify that your changes work as expected in a local development environment (see [Setup](#setup)). **Pull Requests that have not been tested by their creator will be rejected.** This policy keeps the PR history clean and values contributors who submit verified, working code. Untested PRs are often recognizable by disproportionately large or scattered changes for simple tasks—please test first.
- **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`).
+- **Large Features:** Pull Requests that introduce very large or broad features **will not be accepted** unless the idea is first outlined and discussed in a GitHub issue. Large features should be designed together with the Dokploy team so the project stays coherent and moves in the same direction. Open an issue to propose and align on the design before implementing.
Thank you for your contribution!
diff --git a/Dockerfile b/Dockerfile
index 5d7bb6770..ed936508f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,9 +1,9 @@
# syntax=docker/dockerfile:1
-FROM node:20.16.0-slim AS base
+FROM node:24.4.0-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
-RUN corepack prepare pnpm@9.12.0 --activate
+RUN corepack prepare pnpm@10.22.0 --activate
FROM base AS build
COPY . /usr/src/app
@@ -20,7 +20,7 @@ ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/dokploy run build
-RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy
+RUN pnpm --filter=./apps/dokploy --prod deploy --legacy /prod/dokploy
RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next
RUN cp -R /usr/src/app/apps/dokploy/dist /prod/dokploy/dist
@@ -65,4 +65,8 @@ RUN curl -sSL https://railpack.com/install.sh | bash
COPY --from=buildpacksio/pack:0.39.1 /usr/local/bin/pack /usr/local/bin/pack
EXPOSE 3000
-CMD [ "pnpm", "start" ]
+
+HEALTHCHECK --interval=10s --timeout=3s --retries=10 \
+ CMD curl -fs http://localhost:3000/api/trpc/settings.health || exit 1
+
+ CMD ["sh", "-c", "pnpm run wait-for-postgres && exec pnpm start"]
diff --git a/Dockerfile.cloud b/Dockerfile.cloud
index a0de32021..05e7cde49 100644
--- a/Dockerfile.cloud
+++ b/Dockerfile.cloud
@@ -1,9 +1,9 @@
# syntax=docker/dockerfile:1
-FROM node:20.16.0-slim AS base
+FROM node:24.4.0-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
-RUN corepack prepare pnpm@9.12.0 --activate
+RUN corepack prepare pnpm@10.22.0 --activate
FROM base AS build
COPY . /usr/src/app
@@ -29,7 +29,7 @@ ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/dokploy run build
-RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy
+RUN pnpm --filter=./apps/dokploy --prod deploy --legacy /prod/dokploy
RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next
RUN cp -R /usr/src/app/apps/dokploy/dist /prod/dokploy/dist
diff --git a/Dockerfile.schedule b/Dockerfile.schedule
index ce1f96edf..81b13fd64 100644
--- a/Dockerfile.schedule
+++ b/Dockerfile.schedule
@@ -1,9 +1,9 @@
# syntax=docker/dockerfile:1
-FROM node:20.16.0-slim AS base
+FROM node:24.4.0-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
-RUN corepack prepare pnpm@9.12.0 --activate
+RUN corepack prepare pnpm@10.22.0 --activate
FROM base AS build
COPY . /usr/src/app
@@ -20,7 +20,7 @@ ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/schedules run build
-RUN pnpm --filter=./apps/schedules --prod deploy /prod/schedules
+RUN pnpm --filter=./apps/schedules --prod deploy --legacy /prod/schedules
RUN cp -R /usr/src/app/apps/schedules/dist /prod/schedules/dist
diff --git a/Dockerfile.server b/Dockerfile.server
index f5aa25c1e..8990ece4d 100644
--- a/Dockerfile.server
+++ b/Dockerfile.server
@@ -1,9 +1,9 @@
# syntax=docker/dockerfile:1
-FROM node:20.16.0-slim AS base
+FROM node:24.4.0-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
-RUN corepack prepare pnpm@9.12.0 --activate
+RUN corepack prepare pnpm@10.22.0 --activate
FROM base AS build
COPY . /usr/src/app
@@ -20,7 +20,7 @@ ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/api run build
-RUN pnpm --filter=./apps/api --prod deploy /prod/api
+RUN pnpm --filter=./apps/api --prod deploy --legacy /prod/api
RUN cp -R /usr/src/app/apps/api/dist /prod/api/dist
diff --git a/apps/api/package.json b/apps/api/package.json
index 70c8aaac8..c7e76afc7 100644
--- a/apps/api/package.json
+++ b/apps/api/package.json
@@ -4,7 +4,7 @@
"type": "module",
"scripts": {
"dev": "PORT=4000 tsx watch src/index.ts",
- "build": "tsc --project tsconfig.json",
+ "build": "rimraf dist && tsc --project tsconfig.json",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit"
},
@@ -12,7 +12,7 @@
"inngest": "3.40.1",
"@dokploy/server": "workspace:*",
"@hono/node-server": "^1.14.3",
- "@hono/zod-validator": "0.3.0",
+ "@hono/zod-validator": "0.7.6",
"dotenv": "^16.4.5",
"hono": "^4.11.7",
"pino": "9.4.0",
@@ -20,18 +20,19 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"redis": "4.7.0",
- "zod": "^3.25.32"
+ "zod": "^4.3.6"
},
"devDependencies": {
- "@types/node": "^20.16.0",
+ "@types/node": "^24.4.0",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
+ "rimraf": "6.1.3",
"tsx": "^4.16.2",
"typescript": "^5.8.3"
},
- "packageManager": "pnpm@9.12.0",
+ "packageManager": "pnpm@10.22.0",
"engines": {
- "node": "^20.16.0",
- "pnpm": ">=9.12.0"
+ "node": "^24.4.0",
+ "pnpm": ">=10.22.0"
}
}
diff --git a/apps/dokploy/.nvmrc b/apps/dokploy/.nvmrc
deleted file mode 100644
index 593cb75bc..000000000
--- a/apps/dokploy/.nvmrc
+++ /dev/null
@@ -1 +0,0 @@
-20.16.0
\ No newline at end of file
diff --git a/apps/dokploy/__test__/deploy/application.command.test.ts b/apps/dokploy/__test__/deploy/application.command.test.ts
index be29748eb..1a33489b5 100644
--- a/apps/dokploy/__test__/deploy/application.command.test.ts
+++ b/apps/dokploy/__test__/deploy/application.command.test.ts
@@ -14,13 +14,18 @@ vi.mock("@dokploy/server/db", () => {
set: vi.fn(() => chain),
where: vi.fn(() => chain),
returning: vi.fn().mockResolvedValue([{}] as any),
+ from: vi.fn(() => chain),
+ innerJoin: vi.fn(() => chain),
+ then: (resolve: (v: any) => void) => {
+ resolve([]);
+ },
} as any;
return chain;
};
return {
db: {
- select: vi.fn(),
+ select: vi.fn(() => createChainableMock()),
insert: vi.fn(),
update: vi.fn(() => createChainableMock()),
delete: vi.fn(),
@@ -28,6 +33,12 @@ vi.mock("@dokploy/server/db", () => {
applications: {
findFirst: vi.fn(),
},
+ patch: {
+ findMany: vi.fn().mockResolvedValue([]),
+ },
+ member: {
+ findMany: vi.fn().mockResolvedValue([]),
+ },
},
},
};
diff --git a/apps/dokploy/__test__/deploy/application.real.test.ts b/apps/dokploy/__test__/deploy/application.real.test.ts
index 43ff07836..4adff6f07 100644
--- a/apps/dokploy/__test__/deploy/application.real.test.ts
+++ b/apps/dokploy/__test__/deploy/application.real.test.ts
@@ -15,13 +15,18 @@ vi.mock("@dokploy/server/db", () => {
set: vi.fn(() => chain),
where: vi.fn(() => chain),
returning: vi.fn().mockResolvedValue([{}]),
+ from: vi.fn(() => chain),
+ innerJoin: vi.fn(() => chain),
+ then: (resolve: (v: any) => void) => {
+ resolve([]);
+ },
};
return chain;
};
return {
db: {
- select: vi.fn(),
+ select: vi.fn(() => createChainableMock()),
insert: vi.fn(),
update: vi.fn(() => createChainableMock()),
delete: vi.fn(),
@@ -29,6 +34,12 @@ vi.mock("@dokploy/server/db", () => {
applications: {
findFirst: vi.fn(),
},
+ patch: {
+ findMany: vi.fn().mockResolvedValue([]),
+ },
+ member: {
+ findMany: vi.fn().mockResolvedValue([]),
+ },
},
},
};
diff --git a/apps/dokploy/__test__/deploy/github.test.ts b/apps/dokploy/__test__/deploy/github.test.ts
index 46be44883..d2e773dfc 100644
--- a/apps/dokploy/__test__/deploy/github.test.ts
+++ b/apps/dokploy/__test__/deploy/github.test.ts
@@ -83,6 +83,14 @@ describe("GitHub Webhook Skip CI", () => {
{ commits: [{ message: "[skip ci] test" }] },
),
).toBe("[skip ci] test");
+
+ // Soft Serve
+ expect(
+ extractCommitMessage(
+ { "x-softserve-event": "push" },
+ { commits: [{ message: "[skip ci] test" }] },
+ ),
+ ).toBe("[skip ci] test");
});
it("should handle missing commit message", () => {
@@ -99,6 +107,9 @@ describe("GitHub Webhook Skip CI", () => {
expect(extractCommitMessage({ "x-gitea-event": "push" }, {})).toBe(
"NEW COMMIT",
);
+ expect(extractCommitMessage({ "x-softserve-event": "push" }, {})).toBe(
+ "NEW COMMIT",
+ );
});
});
diff --git a/apps/dokploy/__test__/deploy/soft-serve.test.ts b/apps/dokploy/__test__/deploy/soft-serve.test.ts
new file mode 100644
index 000000000..609f15dee
--- /dev/null
+++ b/apps/dokploy/__test__/deploy/soft-serve.test.ts
@@ -0,0 +1,49 @@
+import { describe, expect, it } from "vitest";
+import {
+ extractBranchName,
+ extractCommitMessage,
+ extractHash,
+ getProviderByHeader,
+} from "@/pages/api/deploy/[refreshToken]";
+
+describe("Soft Serve Webhook", () => {
+ const mockSoftServeHeaders = {
+ "x-softserve-event": "push",
+ };
+
+ const createMockBody = (message: string, hash: string, branch: string) => ({
+ event: "push",
+ ref: `refs/heads/${branch}`,
+ after: hash,
+ commits: [{ message: message }],
+ });
+ const message: string = "feat: add new feature";
+ const hash: string = "3c91c24ef9560bddc695bce138bf8a7094ec3df5";
+ const branch: string = "feat/add-new";
+ const goodWebhook = createMockBody(message, hash, branch);
+
+ it("should properly extract the provider name", () => {
+ expect(getProviderByHeader(mockSoftServeHeaders)).toBe("soft-serve");
+ });
+
+ it("should properly extract the commit message", () => {
+ expect(extractCommitMessage(mockSoftServeHeaders, goodWebhook)).toBe(
+ message,
+ );
+ });
+
+ it("should properly extract hash", () => {
+ expect(extractHash(mockSoftServeHeaders, goodWebhook)).toBe(hash);
+ });
+
+ it("should properly extract branch name", () => {
+ expect(extractBranchName(mockSoftServeHeaders, goodWebhook)).toBe(branch);
+ });
+
+ it("should gracefully handle invalid webhook", () => {
+ expect(getProviderByHeader({})).toBeNull();
+ expect(extractCommitMessage(mockSoftServeHeaders, {})).toBe("NEW COMMIT");
+ expect(extractHash(mockSoftServeHeaders, {})).toBe("NEW COMMIT");
+ expect(extractBranchName(mockSoftServeHeaders, {})).toBeNull();
+ });
+});
diff --git a/apps/dokploy/__test__/drop/drop.test.ts b/apps/dokploy/__test__/drop/drop.test.ts
index 85b9b2c61..6e9940d6d 100644
--- a/apps/dokploy/__test__/drop/drop.test.ts
+++ b/apps/dokploy/__test__/drop/drop.test.ts
@@ -6,6 +6,7 @@ import { paths } from "@dokploy/server/constants";
import AdmZip from "adm-zip";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
+const OUTPUT_BASE = "./__test__/drop/zips/output";
const { APPLICATIONS_PATH } = paths();
vi.mock("@dokploy/server/constants", async (importOriginal) => {
const actual = await importOriginal();
@@ -13,7 +14,10 @@ vi.mock("@dokploy/server/constants", async (importOriginal) => {
// @ts-ignore
...actual,
paths: () => ({
- APPLICATIONS_PATH: "./__test__/drop/zips/output",
+ // @ts-ignore
+ ...actual.paths(),
+ BASE_PATH: OUTPUT_BASE,
+ APPLICATIONS_PATH: OUTPUT_BASE,
}),
};
});
@@ -147,8 +151,179 @@ const baseApp: ApplicationNested = {
dockerContextPath: null,
rollbackActive: false,
stopGracePeriodSwarm: null,
+ ulimitsSwarm: null,
};
+/**
+ * GHSA-66v7-g3fh-47h3: Remote Code Execution through Path Traversal.
+ * Validates the exact PoC: ZIP with path traversal entry ../../../../../etc/cron.d/malicious-cron
+ * plus cover files (package.json, index.js). unzipDrop must reject and never write outside output.
+ */
+describe("GHSA-66v7-g3fh-47h3 path traversal RCE", () => {
+ beforeAll(async () => {
+ await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
+ });
+ afterAll(async () => {
+ await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
+ });
+
+ it("rejects PoC ZIP: traversal ../../../../../etc/cron.d/malicious-cron + package.json + index.js", async () => {
+ baseApp.appName = "ghsa-rce";
+ // PoC payload: same entry name as advisory (Python zipfile keeps it; AdmZip normalizes on add → use placeholder + replace)
+ const traversalEntry = "../../../../../etc/cron.d/malicious-cron";
+ const cronPayload = "* * * * * root id\n";
+ const placeholder = "x".repeat(traversalEntry.length);
+ const zip = new AdmZip();
+ zip.addFile(
+ "package.json",
+ Buffer.from('{"name": "app", "version": "1.0.0"}'),
+ );
+ zip.addFile("index.js", Buffer.from('console.log("Application");'));
+ zip.addFile(placeholder, Buffer.from(cronPayload));
+ let buf = Buffer.from(zip.toBuffer());
+ buf = Buffer.from(
+ buf.toString("binary").split(placeholder).join(traversalEntry),
+ "binary",
+ );
+ const file = new File([buf as unknown as ArrayBuffer], "exploit.zip");
+ await expect(unzipDrop(file, baseApp)).rejects.toThrow(
+ /Path traversal detected.*resolved path escapes output directory/,
+ );
+ });
+});
+
+describe("security: existing symlink escape", () => {
+ beforeAll(async () => {
+ await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
+ });
+
+ afterAll(async () => {
+ await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
+ });
+
+ it("should NOT write outside base when directory is a symlink", async () => {
+ const appName = "symlink-existing";
+ const output = path.join(APPLICATIONS_PATH, appName, "code");
+ await fs.mkdir(output, { recursive: true });
+
+ // outside target (attacker wants to write here)
+ const outside = path.join(APPLICATIONS_PATH, "..", "outside");
+ await fs.mkdir(outside, { recursive: true });
+
+ // attacker-controlled symlink inside project
+ await fs.symlink(outside, path.join(output, "logs"));
+
+ // zip looks totally harmless
+ const zip = new AdmZip();
+ zip.addFile("logs/pwned.txt", Buffer.from("owned"));
+
+ const file = new File([zip.toBuffer() as any], "exploit.zip");
+
+ await unzipDrop(file, { ...baseApp, appName });
+
+ // if vulnerable -> file exists outside sandbox
+ const escaped = await fs
+ .readFile(path.join(outside, "pwned.txt"), "utf8")
+ .then(() => true)
+ .catch(() => false);
+
+ expect(escaped).toBe(false);
+ });
+});
+
+describe("security: zip symlink entry blocked", () => {
+ beforeAll(async () => {
+ await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
+ });
+
+ afterAll(async () => {
+ await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
+ });
+
+ it("rejects zip containing real symlink entry", async () => {
+ const appName = "zip-symlink";
+
+ const zipBuffer = await fs.readFile(
+ path.join(__dirname, "./zips/payload/symlink-entry.zip"),
+ );
+
+ const file = new File([zipBuffer as any], "exploit.zip");
+
+ await expect(unzipDrop(file, { ...baseApp, appName })).rejects.toThrow(
+ /Dangerous node entries are not allowed/,
+ );
+ });
+});
+
+describe("unzipDrop path under output (no traversal)", () => {
+ beforeAll(async () => {
+ await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
+ });
+ afterAll(async () => {
+ await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
+ });
+
+ it("allows entry etc/cron.d/malicious-cron when under output (no path traversal)", async () => {
+ baseApp.appName = "cron-under-output";
+ const zip = new AdmZip();
+ zip.addFile(
+ "etc/cron.d/malicious-cron",
+ Buffer.from("* * * * * root id\n"),
+ );
+ zip.addFile("package.json", Buffer.from('{"name":"app"}'));
+ const file = new File(
+ [zip.toBuffer() as unknown as ArrayBuffer],
+ "app.zip",
+ );
+ const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
+ await unzipDrop(file, baseApp);
+ const content = await fs.readFile(
+ path.join(outputPath, "etc/cron.d/malicious-cron"),
+ "utf8",
+ );
+ expect(content).toBe("* * * * * root id\n");
+ });
+});
+
+describe("security: traversal inside BASE_PATH (sandbox escape)", () => {
+ beforeAll(async () => {
+ await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
+ });
+
+ afterAll(async () => {
+ await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
+ });
+
+ it("should NOT allow writing outside application directory but inside BASE_PATH", async () => {
+ const appName = "sandbox-escape";
+
+ const base = APPLICATIONS_PATH.replace("/applications", "");
+ const output = path.join(APPLICATIONS_PATH, appName, "code");
+
+ await fs.mkdir(output, { recursive: true });
+
+ // attacker writes into traefik config inside base
+ const zip = new AdmZip();
+ zip.addFile(
+ "../../../traefik/dynamic/evil.yml",
+ Buffer.from("pwned: true"),
+ );
+
+ const file = new File([zip.toBuffer() as any], "exploit.zip");
+
+ await unzipDrop(file, { ...baseApp, appName });
+
+ const escapedPath = path.join(base, "traefik/dynamic/evil.yml");
+
+ const exists = await fs
+ .readFile(escapedPath)
+ .then(() => true)
+ .catch(() => false);
+
+ expect(exists).toBe(false);
+ });
+});
+
describe("unzipDrop using real zip files", () => {
// const { APPLICATIONS_PATH } = paths();
beforeAll(async () => {
@@ -165,14 +340,12 @@ describe("unzipDrop using real zip files", () => {
try {
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() as Buffer;
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "test.txt")).toBe(true);
} catch (err) {
- console.log(err);
} finally {
}
});
diff --git a/apps/dokploy/__test__/drop/zips/payload/link b/apps/dokploy/__test__/drop/zips/payload/link
new file mode 120000
index 000000000..3594e94c0
--- /dev/null
+++ b/apps/dokploy/__test__/drop/zips/payload/link
@@ -0,0 +1 @@
+/etc/passwd
\ No newline at end of file
diff --git a/apps/dokploy/__test__/drop/zips/payload/symlink-entry.zip b/apps/dokploy/__test__/drop/zips/payload/symlink-entry.zip
new file mode 100644
index 000000000..b30279c6b
Binary files /dev/null and b/apps/dokploy/__test__/drop/zips/payload/symlink-entry.zip differ
diff --git a/apps/dokploy/__test__/server/mechanizeDockerContainer.test.ts b/apps/dokploy/__test__/server/mechanizeDockerContainer.test.ts
index dcaf59d83..fb448e3af 100644
--- a/apps/dokploy/__test__/server/mechanizeDockerContainer.test.ts
+++ b/apps/dokploy/__test__/server/mechanizeDockerContainer.test.ts
@@ -6,6 +6,7 @@ type MockCreateServiceOptions = {
TaskTemplate?: {
ContainerSpec?: {
StopGracePeriod?: number;
+ Ulimits?: Array<{ Name: string; Soft: number; Hard: number }>;
};
};
[key: string]: unknown;
@@ -57,6 +58,7 @@ const createApplication = (
},
replicas: 1,
stopGracePeriodSwarm: 0n,
+ ulimitsSwarm: null,
serverId: "server-id",
...overrides,
}) as unknown as ApplicationNested;
@@ -110,4 +112,50 @@ describe("mechanizeDockerContainer", () => {
"StopGracePeriod",
);
});
+
+ it("passes ulimits to ContainerSpec when ulimitsSwarm is defined", async () => {
+ const ulimits = [
+ { Name: "nofile", Soft: 10000, Hard: 20000 },
+ { Name: "nproc", Soft: 4096, Hard: 8192 },
+ ];
+ const application = createApplication({ ulimitsSwarm: ulimits });
+
+ await mechanizeDockerContainer(application);
+
+ expect(createServiceMock).toHaveBeenCalledTimes(1);
+ const call = createServiceMock.mock.calls[0];
+ if (!call) {
+ throw new Error("createServiceMock should have been called once");
+ }
+ const [settings] = call;
+ expect(settings.TaskTemplate?.ContainerSpec?.Ulimits).toEqual(ulimits);
+ });
+
+ it("omits Ulimits when ulimitsSwarm is null", async () => {
+ const application = createApplication({ ulimitsSwarm: null });
+
+ await mechanizeDockerContainer(application);
+
+ expect(createServiceMock).toHaveBeenCalledTimes(1);
+ const call = createServiceMock.mock.calls[0];
+ if (!call) {
+ throw new Error("createServiceMock should have been called once");
+ }
+ const [settings] = call;
+ expect(settings.TaskTemplate?.ContainerSpec).not.toHaveProperty("Ulimits");
+ });
+
+ it("omits Ulimits when ulimitsSwarm is an empty array", async () => {
+ const application = createApplication({ ulimitsSwarm: [] });
+
+ await mechanizeDockerContainer(application);
+
+ expect(createServiceMock).toHaveBeenCalledTimes(1);
+ const call = createServiceMock.mock.calls[0];
+ if (!call) {
+ throw new Error("createServiceMock should have been called once");
+ }
+ const [settings] = call;
+ expect(settings.TaskTemplate?.ContainerSpec).not.toHaveProperty("Ulimits");
+ });
});
diff --git a/apps/dokploy/__test__/setup.ts b/apps/dokploy/__test__/setup.ts
index 5af01d147..04fd08b0c 100644
--- a/apps/dokploy/__test__/setup.ts
+++ b/apps/dokploy/__test__/setup.ts
@@ -12,7 +12,11 @@ vi.mock("@dokploy/server/db", () => {
chain.where = () => chain;
chain.values = () => chain;
chain.returning = () => Promise.resolve([{}]);
- chain.then = undefined;
+ chain.from = () => chain;
+ chain.innerJoin = () => chain;
+ chain.then = (resolve: (value: unknown) => void) => {
+ resolve([]);
+ };
const tableMock = {
findFirst: vi.fn(() => Promise.resolve(undefined)),
@@ -21,7 +25,6 @@ vi.mock("@dokploy/server/db", () => {
update: vi.fn(() => chain),
delete: vi.fn(() => chain),
};
- const createQueryMock = () => tableMock;
return {
db: {
diff --git a/apps/dokploy/__test__/traefik/traefik.test.ts b/apps/dokploy/__test__/traefik/traefik.test.ts
index 0e6e529b0..9121dc8a1 100644
--- a/apps/dokploy/__test__/traefik/traefik.test.ts
+++ b/apps/dokploy/__test__/traefik/traefik.test.ts
@@ -125,6 +125,7 @@ const baseApp: ApplicationNested = {
username: null,
dockerContextPath: null,
stopGracePeriodSwarm: null,
+ ulimitsSwarm: null,
};
const baseDomain: Domain = {
@@ -274,3 +275,51 @@ test("CertificateType on websecure entrypoint", async () => {
expect(router.tls?.certResolver).toBe("letsencrypt");
});
+
+/** IDN/Punycode */
+
+test("Internationalized domain name is converted to punycode", async () => {
+ const router = await createRouterConfig(
+ baseApp,
+ { ...baseDomain, host: "тест.рф" },
+ "web",
+ );
+
+ // тест.рф in punycode is xn--e1aybc.xn--p1ai
+ expect(router.rule).toContain("Host(`xn--e1aybc.xn--p1ai`)");
+ expect(router.rule).not.toContain("тест.рф");
+});
+
+test("ASCII domain remains unchanged", async () => {
+ const router = await createRouterConfig(
+ baseApp,
+ { ...baseDomain, host: "example.com" },
+ "web",
+ );
+
+ expect(router.rule).toContain("Host(`example.com`)");
+});
+
+test("Russian Cyrillic label with .ru TLD is converted to punycode", async () => {
+ const router = await createRouterConfig(
+ baseApp,
+ { ...baseDomain, host: "сайт.ru" },
+ "web",
+ );
+
+ // сайт in punycode is xn--80aswg
+ expect(router.rule).toContain("Host(`xn--80aswg.ru`)");
+ expect(router.rule).not.toContain("сайт");
+});
+
+test("Subdomain with Russian IDN TLD converts non-ASCII part to punycode", async () => {
+ const router = await createRouterConfig(
+ baseApp,
+ { ...baseDomain, host: "app.тест.рф" },
+ "web",
+ );
+
+ // app stays ASCII, тест.рф becomes xn--e1aybc.xn--p1ai
+ expect(router.rule).toContain("Host(`app.xn--e1aybc.xn--p1ai`)");
+ expect(router.rule).not.toContain("тест.рф");
+});
diff --git a/apps/dokploy/__test__/wss/readValidDirectory.test.ts b/apps/dokploy/__test__/wss/readValidDirectory.test.ts
new file mode 100644
index 000000000..8107bb591
--- /dev/null
+++ b/apps/dokploy/__test__/wss/readValidDirectory.test.ts
@@ -0,0 +1,81 @@
+import path from "node:path";
+import { describe, expect, it, vi } from "vitest";
+
+const BASE = "/base";
+
+vi.mock("@dokploy/server/constants", async (importOriginal) => {
+ const actual =
+ await importOriginal();
+ return {
+ ...actual,
+ paths: () => ({
+ ...actual.paths(),
+ BASE_PATH: BASE,
+ LOGS_PATH: `${BASE}/logs`,
+ APPLICATIONS_PATH: `${BASE}/applications`,
+ }),
+ };
+});
+
+// Import after mock so paths() uses our BASE
+const { readValidDirectory } = await import("@dokploy/server");
+
+describe("readValidDirectory (path traversal)", () => {
+ it("returns true when directory is exactly BASE_PATH", () => {
+ expect(readValidDirectory(BASE)).toBe(true);
+ expect(readValidDirectory(path.resolve(BASE))).toBe(true);
+ });
+
+ it("returns true when directory is under BASE_PATH", () => {
+ expect(readValidDirectory(`${BASE}/logs`)).toBe(true);
+ expect(readValidDirectory(`${BASE}/logs/app/foo.log`)).toBe(true);
+ expect(readValidDirectory(`${BASE}/applications/myapp/code`)).toBe(true);
+ });
+
+ it("returns false for path traversal escaping base (absolute)", () => {
+ expect(readValidDirectory("/etc/passwd")).toBe(false);
+ expect(readValidDirectory("/etc/cron.d/malicious")).toBe(false);
+ expect(readValidDirectory("/tmp/outside")).toBe(false);
+ });
+
+ it("returns false when resolved path escapes base via ..", () => {
+ // Resolved: /etc/passwd (outside /base)
+ expect(readValidDirectory(`${BASE}/../etc/passwd`)).toBe(false);
+ expect(readValidDirectory(`${BASE}/logs/../../etc/passwd`)).toBe(false);
+ expect(readValidDirectory(`${BASE}/..`)).toBe(false);
+ });
+
+ it("returns true when .. stays within base", () => {
+ // e.g. /base/logs/../applications -> /base/applications (still under /base)
+ expect(readValidDirectory(`${BASE}/logs/../applications`)).toBe(true);
+ expect(readValidDirectory(`${BASE}/foo/../bar`)).toBe(true);
+ });
+
+ it("accepts serverId for remote base path", () => {
+ // With our mock, serverId doesn't change BASE_PATH; just ensure it doesn't throw
+ expect(readValidDirectory(BASE, "server-1")).toBe(true);
+ expect(readValidDirectory("/etc/passwd", "server-1")).toBe(false);
+ });
+
+ it("returns false for null/undefined-like paths that resolve outside", () => {
+ // Paths that might resolve to cwd or root
+ expect(readValidDirectory(".")).toBe(false);
+ expect(readValidDirectory("..")).toBe(false);
+ });
+
+ it("returns true for BASE_PATH with trailing slash or double slashes under base", () => {
+ expect(readValidDirectory(`${BASE}/`)).toBe(true);
+ expect(readValidDirectory(`${BASE}//logs`)).toBe(true);
+ expect(readValidDirectory(`${BASE}/applications///myapp/code`)).toBe(true);
+ });
+
+ it("returns false when path looks like base but is a sibling or prefix", () => {
+ expect(readValidDirectory("/base-evil")).toBe(false);
+ expect(readValidDirectory("/bas")).toBe(false);
+ expect(readValidDirectory(`${BASE}/../base-evil`)).toBe(false);
+ });
+
+ it("returns false for empty string (resolves to cwd)", () => {
+ expect(readValidDirectory("")).toBe(false);
+ });
+});
diff --git a/apps/dokploy/__test__/wss/utils.test.ts b/apps/dokploy/__test__/wss/utils.test.ts
new file mode 100644
index 000000000..209bd5f86
--- /dev/null
+++ b/apps/dokploy/__test__/wss/utils.test.ts
@@ -0,0 +1,132 @@
+import { describe, expect, it } from "vitest";
+import {
+ isValidContainerId,
+ isValidSearch,
+ isValidSince,
+ isValidTail,
+} from "../../server/wss/utils";
+
+describe("isValidTail (docker-container-logs)", () => {
+ it("accepts valid numeric tail values", () => {
+ expect(isValidTail("0")).toBe(true);
+ expect(isValidTail("1")).toBe(true);
+ expect(isValidTail("100")).toBe(true);
+ expect(isValidTail("10000")).toBe(true);
+ });
+
+ it("rejects tail above 10000", () => {
+ expect(isValidTail("10001")).toBe(false);
+ expect(isValidTail("99999")).toBe(false);
+ });
+
+ it("rejects non-numeric tail", () => {
+ expect(isValidTail("")).toBe(false);
+ expect(isValidTail("abc")).toBe(false);
+ expect(isValidTail("10a")).toBe(false);
+ expect(isValidTail("-1")).toBe(false);
+ });
+
+ it("rejects command injection payloads in tail", () => {
+ expect(isValidTail("10; whoami; #")).toBe(false);
+ expect(isValidTail("100 | cat /etc/passwd")).toBe(false);
+ expect(isValidTail("$(id)")).toBe(false);
+ expect(isValidTail("`id`")).toBe(false);
+ expect(isValidTail("100\nid")).toBe(false);
+ expect(isValidTail("100 && id")).toBe(false);
+ expect(isValidTail("100; env | grep DATABASE")).toBe(false);
+ });
+});
+
+describe("isValidSince (docker-container-logs)", () => {
+ it("accepts 'all'", () => {
+ expect(isValidSince("all")).toBe(true);
+ });
+
+ it("accepts valid duration format (number + s|m|h|d)", () => {
+ expect(isValidSince("5s")).toBe(true);
+ expect(isValidSince("10m")).toBe(true);
+ expect(isValidSince("1h")).toBe(true);
+ expect(isValidSince("2d")).toBe(true);
+ expect(isValidSince("0s")).toBe(true);
+ expect(isValidSince("999d")).toBe(true);
+ });
+
+ it("rejects invalid duration format", () => {
+ expect(isValidSince("")).toBe(false);
+ expect(isValidSince("5")).toBe(false);
+ expect(isValidSince("s")).toBe(false);
+ expect(isValidSince("5x")).toBe(false);
+ expect(isValidSince("5sec")).toBe(false);
+ expect(isValidSince("5 m")).toBe(false);
+ });
+
+ it("rejects command injection payloads in since", () => {
+ expect(isValidSince("5s; whoami")).toBe(false);
+ expect(isValidSince("all; id")).toBe(false);
+ expect(isValidSince("1m$(id)")).toBe(false);
+ expect(isValidSince("1m | cat /etc/passwd")).toBe(false);
+ });
+});
+
+describe("isValidSearch (docker-container-logs)", () => {
+ it("accepts empty string", () => {
+ expect(isValidSearch("")).toBe(true);
+ });
+
+ it("accepts only alphanumeric, space, dot, underscore, hyphen", () => {
+ expect(isValidSearch("error")).toBe(true);
+ expect(isValidSearch("foo bar")).toBe(true);
+ expect(isValidSearch("a-zA-Z0-9_.-")).toBe(true);
+ expect(isValidSearch("")).toBe(true);
+ });
+
+ it("rejects strings longer than 500 chars", () => {
+ expect(isValidSearch("a".repeat(501))).toBe(false);
+ expect(isValidSearch("a".repeat(500))).toBe(true);
+ });
+
+ it("rejects control characters and non-printable", () => {
+ expect(isValidSearch("foo\nbar")).toBe(false);
+ expect(isValidSearch("foo\rbar")).toBe(false);
+ expect(isValidSearch("\x00")).toBe(false);
+ expect(isValidSearch("a\x19b")).toBe(false);
+ });
+
+ it("rejects command injection vectors in search (search is concatenated into shell)", () => {
+ // Double-quoted context (SSH line 99): $ and ` execute
+ expect(isValidSearch("$(whoami)")).toBe(false);
+ expect(isValidSearch("`id`")).toBe(false);
+ expect(isValidSearch("$(id)")).toBe(false);
+ // Single-quoted context (local line 153): ' breaks out
+ expect(isValidSearch("'$(whoami)'")).toBe(false);
+ expect(isValidSearch("error'")).toBe(false);
+ expect(isValidSearch("'; whoami; #")).toBe(false);
+ // Other shell-metacharacters
+ expect(isValidSearch("error; id")).toBe(false);
+ expect(isValidSearch("a|b")).toBe(false);
+ expect(isValidSearch('error"')).toBe(false);
+ expect(isValidSearch("a&b")).toBe(false);
+ });
+});
+
+describe("isValidContainerId (docker-container-logs)", () => {
+ it("accepts valid hex container IDs", () => {
+ expect(isValidContainerId("a".repeat(12))).toBe(true);
+ expect(isValidContainerId("abc123def456")).toBe(true);
+ expect(isValidContainerId("a".repeat(64))).toBe(true);
+ });
+
+ it("accepts valid container names", () => {
+ expect(isValidContainerId("my-container")).toBe(true);
+ expect(isValidContainerId("app_1")).toBe(true);
+ expect(isValidContainerId("service.name")).toBe(true);
+ });
+
+ it("rejects command injection in container ID", () => {
+ expect(isValidContainerId("dummy; whoami")).toBe(false);
+ expect(isValidContainerId("$(id)")).toBe(false);
+ expect(isValidContainerId("`id`")).toBe(false);
+ expect(isValidContainerId("container|cat /etc/passwd")).toBe(false);
+ expect(isValidContainerId("x; env | grep DATABASE")).toBe(false);
+ });
+});
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 a3bc8079a..8de863957 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,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Server } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -73,7 +73,7 @@ export const ShowClusterSettings = ({ id, type }: Props) => {
mongo: () => api.mongo.update.useMutation(),
};
- const { mutateAsync, isLoading } = mutationMap[type]
+ const { mutateAsync, isPending } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();
@@ -236,7 +236,7 @@ export const ShowClusterSettings = ({ id, type }: Props) => {
)}
-
+
Save
diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/endpoint-spec-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/endpoint-spec-form.tsx
index 7ee31e5b6..6d95634be 100644
--- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/endpoint-spec-form.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/endpoint-spec-form.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/health-check-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/health-check-form.tsx
index 1e0d032f0..f62037fca 100644
--- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/health-check-form.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/health-check-form.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/labels-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/labels-form.tsx
index d1681dcd0..41ce741ae 100644
--- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/labels-form.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/labels-form.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/mode-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/mode-form.tsx
index 839f5d519..a6885a7e4 100644
--- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/mode-form.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/mode-form.tsx
@@ -105,7 +105,14 @@ export const ModeForm = ({ id, type }: ModeFormProps) => {
const modeData =
formData.type === "Replicated"
- ? { Replicated: { Replicas: formData.Replicas } }
+ ? {
+ Replicated: {
+ Replicas:
+ formData.Replicas !== undefined && formData.Replicas !== ""
+ ? Number(formData.Replicas)
+ : undefined,
+ },
+ }
: { Global: {} };
await mutateAsync({
diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx
index f2c640cfe..7d6ebbaf3 100644
--- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/placement-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/placement-form.tsx
index 25a72b3c9..b4091aac0 100644
--- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/placement-form.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/placement-form.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/restart-policy-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/restart-policy-form.tsx
index b7fb649be..db7be5629 100644
--- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/restart-policy-form.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/restart-policy-form.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/rollback-config-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/rollback-config-form.tsx
index d53215348..528b9d1cc 100644
--- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/rollback-config-form.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/rollback-config-form.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/update-config-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/update-config-form.tsx
index 4119c41f8..af2d826db 100644
--- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/update-config-form.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/update-config-form.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
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 a7c5f7288..602e6877d 100644
--- a/apps/dokploy/components/dashboard/application/advanced/general/add-command.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/general/add-command.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Plus, Trash2 } from "lucide-react";
import { useEffect } from "react";
import { useFieldArray, useForm } from "react-hook-form";
@@ -50,7 +50,7 @@ export const AddCommand = ({ applicationId }: Props) => {
const utils = api.useUtils();
- const { mutateAsync, isLoading } = api.application.update.useMutation();
+ const { mutateAsync, isPending } = api.application.update.useMutation();
const form = useForm({
defaultValues: {
@@ -177,7 +177,7 @@ export const AddCommand = ({ applicationId }: Props) => {
-
+
Save
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 17d033cf2..7b1614fda 100644
--- a/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Code2, Globe2, HardDrive } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -69,11 +69,11 @@ export const ShowImport = ({ composeId }: Props) => {
} | null>(null);
const utils = api.useUtils();
- const { mutateAsync: processTemplate, isLoading: isLoadingTemplate } =
+ const { mutateAsync: processTemplate, isPending: isLoadingTemplate } =
api.compose.processTemplate.useMutation();
const {
mutateAsync: importTemplate,
- isLoading: isImporting,
+ isPending: isImporting,
isSuccess: isImportSuccess,
} = api.compose.import.useMutation();
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 568792461..91570d2db 100644
--- a/apps/dokploy/components/dashboard/application/advanced/ports/handle-ports.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/ports/handle-ports.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm, useWatch } from "react-hook-form";
@@ -35,13 +35,9 @@ import { api } from "@/utils/api";
const AddPortSchema = z.object({
publishedPort: z.number().int().min(1).max(65535),
- publishMode: z.enum(["ingress", "host"], {
- required_error: "Publish mode is required",
- }),
+ publishMode: z.enum(["ingress", "host"]),
targetPort: z.number().int().min(1).max(65535),
- protocol: z.enum(["tcp", "udp"], {
- required_error: "Protocol is required",
- }),
+ protocol: z.enum(["tcp", "udp"]),
});
type AddPort = z.infer;
@@ -68,7 +64,7 @@ export const HandlePorts = ({
enabled: !!portId,
},
);
- const { mutateAsync, isLoading, error, isError } = portId
+ const { mutateAsync, isPending, error, isError } = portId
? api.port.update.useMutation()
: api.port.create.useMutation();
@@ -270,7 +266,7 @@ export const HandlePorts = ({
diff --git a/apps/dokploy/components/dashboard/application/advanced/ports/show-port.tsx b/apps/dokploy/components/dashboard/application/advanced/ports/show-port.tsx
index 816949f2b..4816d224d 100644
--- a/apps/dokploy/components/dashboard/application/advanced/ports/show-port.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/ports/show-port.tsx
@@ -25,7 +25,7 @@ export const ShowPorts = ({ applicationId }: Props) => {
{ enabled: !!applicationId },
);
- const { mutateAsync: deletePort, isLoading: isRemoving } =
+ const { mutateAsync: deletePort, isPending: isRemoving } =
api.port.delete.useMutation();
return (
diff --git a/apps/dokploy/components/dashboard/application/advanced/redirects/handle-redirect.tsx b/apps/dokploy/components/dashboard/application/advanced/redirects/handle-redirect.tsx
index c4d38ef18..172c042f1 100644
--- a/apps/dokploy/components/dashboard/application/advanced/redirects/handle-redirect.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/redirects/handle-redirect.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -100,11 +100,11 @@ export const HandleRedirect = ({
const utils = api.useUtils();
- const { mutateAsync, isLoading, error, isError } = redirectId
+ const { mutateAsync, isPending, error, isError } = redirectId
? api.redirects.update.useMutation()
: api.redirects.create.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
permanent: false,
regex: "",
@@ -268,7 +268,7 @@ export const HandleRedirect = ({
diff --git a/apps/dokploy/components/dashboard/application/advanced/redirects/show-redirects.tsx b/apps/dokploy/components/dashboard/application/advanced/redirects/show-redirects.tsx
index f1b14bfc0..a14074ec5 100644
--- a/apps/dokploy/components/dashboard/application/advanced/redirects/show-redirects.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/redirects/show-redirects.tsx
@@ -24,7 +24,7 @@ export const ShowRedirects = ({ applicationId }: Props) => {
{ enabled: !!applicationId },
);
- const { mutateAsync: deleteRedirect, isLoading: isRemoving } =
+ const { mutateAsync: deleteRedirect, isPending: isRemoving } =
api.redirects.delete.useMutation();
const utils = api.useUtils();
diff --git a/apps/dokploy/components/dashboard/application/advanced/security/handle-security.tsx b/apps/dokploy/components/dashboard/application/advanced/security/handle-security.tsx
index c52976eb1..49a126881 100644
--- a/apps/dokploy/components/dashboard/application/advanced/security/handle-security.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/security/handle-security.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -46,7 +46,7 @@ export const HandleSecurity = ({
}: Props) => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
- const { data } = api.security.one.useQuery(
+ const { data, refetch } = api.security.one.useQuery(
{
securityId: securityId ?? "",
},
@@ -55,7 +55,7 @@ export const HandleSecurity = ({
},
);
- const { mutateAsync, isLoading, error, isError } = securityId
+ const { mutateAsync, isPending, error, isError } = securityId
? api.security.update.useMutation()
: api.security.create.useMutation();
@@ -88,6 +88,7 @@ export const HandleSecurity = ({
await utils.application.readTraefikConfig.invalidate({
applicationId,
});
+ await refetch();
setIsOpen(false);
})
.catch(() => {
@@ -163,7 +164,7 @@ export const HandleSecurity = ({
diff --git a/apps/dokploy/components/dashboard/application/advanced/security/show-security.tsx b/apps/dokploy/components/dashboard/application/advanced/security/show-security.tsx
index 5676e6f00..724953afe 100644
--- a/apps/dokploy/components/dashboard/application/advanced/security/show-security.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/security/show-security.tsx
@@ -27,7 +27,7 @@ export const ShowSecurity = ({ applicationId }: Props) => {
{ enabled: !!applicationId },
);
- const { mutateAsync: deleteSecurity, isLoading: isRemoving } =
+ const { mutateAsync: deleteSecurity, isPending: isRemoving } =
api.security.delete.useMutation();
const utils = api.useUtils();
diff --git a/apps/dokploy/components/dashboard/application/advanced/show-build-server.tsx b/apps/dokploy/components/dashboard/application/advanced/show-build-server.tsx
index 545a5f705..eaeafde1a 100644
--- a/apps/dokploy/components/dashboard/application/advanced/show-build-server.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/show-build-server.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Server } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -74,7 +74,7 @@ export const ShowBuildServer = ({ applicationId }: Props) => {
const { data: buildServers } = api.server.buildServers.useQuery();
const { data: registries } = api.registry.all.useQuery();
- const { mutateAsync, isLoading } = api.application.update.useMutation();
+ const { mutateAsync, isPending } = api.application.update.useMutation();
const form = useForm({
defaultValues: {
@@ -274,7 +274,7 @@ export const ShowBuildServer = ({ applicationId }: Props) => {
/>
-
+
Save
diff --git a/apps/dokploy/components/dashboard/application/advanced/show-resources.tsx b/apps/dokploy/components/dashboard/application/advanced/show-resources.tsx
index aea30e49b..3b30155bf 100644
--- a/apps/dokploy/components/dashboard/application/advanced/show-resources.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/show-resources.tsx
@@ -1,7 +1,7 @@
-import { zodResolver } from "@hookform/resolvers/zod";
-import { InfoIcon } from "lucide-react";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
+import { InfoIcon, Plus, Trash2 } from "lucide-react";
import { useEffect } from "react";
-import { useForm } from "react-hook-form";
+import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
@@ -21,10 +21,18 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
import {
createConverter,
NumberInputWithSteps,
} from "@/components/ui/number-input";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
@@ -50,13 +58,36 @@ const memoryConverter = createConverter(1024 * 1024, (mb) => {
: `${formatNumber(mb)} MB`;
});
+const ulimitSchema = z.object({
+ Name: z.string().min(1, "Name is required"),
+ Soft: z.coerce.number().int().min(-1, "Must be >= -1"),
+ Hard: z.coerce.number().int().min(-1, "Must be >= -1"),
+});
+
const addResourcesSchema = z.object({
memoryReservation: z.string().optional(),
cpuLimit: z.string().optional(),
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
+ ulimitsSwarm: z.array(ulimitSchema).optional(),
});
+const ULIMIT_PRESETS = [
+ { value: "nofile", label: "nofile (Open Files)" },
+ { value: "nproc", label: "nproc (Processes)" },
+ { value: "memlock", label: "memlock (Locked Memory)" },
+ { value: "stack", label: "stack (Stack Size)" },
+ { value: "core", label: "core (Core File Size)" },
+ { value: "cpu", label: "cpu (CPU Time)" },
+ { value: "data", label: "data (Data Segment)" },
+ { value: "fsize", label: "fsize (File Size)" },
+ { value: "locks", label: "locks (File Locks)" },
+ { value: "msgqueue", label: "msgqueue (Message Queues)" },
+ { value: "nice", label: "nice (Nice Priority)" },
+ { value: "rtprio", label: "rtprio (Real-time Priority)" },
+ { value: "sigpending", label: "sigpending (Pending Signals)" },
+];
+
export type ServiceType =
| "postgres"
| "mongo"
@@ -97,20 +128,26 @@ export const ShowResources = ({ id, type }: Props) => {
mongo: () => api.mongo.update.useMutation(),
};
- const { mutateAsync, isLoading } = mutationMap[type]
+ const { mutateAsync, isPending } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
cpuLimit: "",
cpuReservation: "",
memoryLimit: "",
memoryReservation: "",
+ ulimitsSwarm: [],
},
resolver: zodResolver(addResourcesSchema),
});
+ const { fields, append, remove } = useFieldArray({
+ control: form.control,
+ name: "ulimitsSwarm",
+ });
+
useEffect(() => {
if (data) {
form.reset({
@@ -118,6 +155,7 @@ export const ShowResources = ({ id, type }: Props) => {
cpuReservation: data?.cpuReservation || undefined,
memoryLimit: data?.memoryLimit || undefined,
memoryReservation: data?.memoryReservation || undefined,
+ ulimitsSwarm: data?.ulimitsSwarm || [],
});
}
}, [data, form, form.reset]);
@@ -134,6 +172,10 @@ export const ShowResources = ({ id, type }: Props) => {
cpuReservation: formData.cpuReservation || null,
memoryLimit: formData.memoryLimit || null,
memoryReservation: formData.memoryReservation || null,
+ ulimitsSwarm:
+ formData.ulimitsSwarm && formData.ulimitsSwarm.length > 0
+ ? formData.ulimitsSwarm
+ : null,
})
.then(async () => {
toast.success("Resources Updated");
@@ -325,8 +367,157 @@ export const ShowResources = ({ id, type }: Props) => {
}}
/>
+
+ {/* Ulimits Section */}
+
+
+
+
Ulimits
+
+
+
+
+
+
+
+ Set resource limits for the container. Each ulimit has
+ a soft limit (warning threshold) and hard limit
+ (maximum allowed). Use -1 for unlimited.
+
+
+
+
+
+
+ append({ Name: "nofile", Soft: 65535, Hard: 65535 })
+ }
+ >
+
+ Add Ulimit
+
+
+
+ {fields.length > 0 && (
+
+ )}
+
+ {fields.length === 0 && (
+
+ No ulimits configured. Click "Add Ulimit" to set
+ resource limits.
+
+ )}
+
+
-
+
Save
diff --git a/apps/dokploy/components/dashboard/application/advanced/traefik/show-traefik-config.tsx b/apps/dokploy/components/dashboard/application/advanced/traefik/show-traefik-config.tsx
index ae23f1866..5d8943197 100644
--- a/apps/dokploy/components/dashboard/application/advanced/traefik/show-traefik-config.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/traefik/show-traefik-config.tsx
@@ -15,7 +15,7 @@ interface Props {
}
export const ShowTraefikConfig = ({ applicationId }: Props) => {
- const { data, isLoading } = api.application.readTraefikConfig.useQuery(
+ const { data, isPending } = api.application.readTraefikConfig.useQuery(
{
applicationId,
},
@@ -35,7 +35,7 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => {
- {isLoading ? (
+ {isPending ? (
Loading...
diff --git a/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx b/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx
index 928949d9f..a8ec9053f 100644
--- a/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -7,6 +7,7 @@ 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 { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
@@ -24,7 +25,6 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
-import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
@@ -69,7 +69,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
{ enabled: !!applicationId },
);
- const { mutateAsync, isLoading, error, isError } =
+ const { mutateAsync, isPending, error, isError } =
api.application.updateTraefikConfig.useMutation();
const form = useForm({
@@ -126,7 +126,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
}}
>
- Modify
+ Modify
@@ -198,7 +198,7 @@ routers:
diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx
index 2bfd6bbc0..7c8dff068 100644
--- a/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PlusIcon } from "lucide-react";
import type React from "react";
import { useEffect, useState } from "react";
diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx
index d3803c42a..92b259140 100644
--- a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx
@@ -37,7 +37,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
const { data, refetch } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
- const { mutateAsync: deleteVolume, isLoading: isRemoving } =
+ const { mutateAsync: deleteVolume, isPending: isRemoving } =
api.mounts.remove.useMutation();
return (
diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx
index 44fb050bc..9f31cc694 100644
--- a/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -93,7 +93,7 @@ export const UpdateVolume = ({
},
);
- const { mutateAsync, isLoading, error, isError } =
+ const { mutateAsync, isPending, error, isError } =
api.mounts.update.useMutation();
const form = useForm({
@@ -187,7 +187,7 @@ export const UpdateVolume = ({
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10 "
- isLoading={isLoading}
+ isLoading={isPending}
>
@@ -310,7 +310,7 @@ PORT=3000
diff --git a/apps/dokploy/components/dashboard/application/build/show.tsx b/apps/dokploy/components/dashboard/application/build/show.tsx
index 7f92157f2..32aee23d3 100644
--- a/apps/dokploy/components/dashboard/application/build/show.tsx
+++ b/apps/dokploy/components/dashboard/application/build/show.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Cog } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -74,12 +74,7 @@ const buildTypeDisplayMap: Record = {
const mySchema = z.discriminatedUnion("buildType", [
z.object({
buildType: z.literal(BuildType.dockerfile),
- dockerfile: z
- .string({
- required_error: "Dockerfile path is required",
- invalid_type_error: "Dockerfile path is required",
- })
- .min(1, "Dockerfile required"),
+ dockerfile: z.string().nullable().default(""),
dockerContextPath: z.string().nullable().default(""),
dockerBuildStage: z.string().nullable().default(""),
}),
@@ -168,14 +163,14 @@ const resetData = (data: ApplicationData): AddTemplate => {
};
export const ShowBuildChooseForm = ({ applicationId }: Props) => {
- const { mutateAsync, isLoading } =
+ const { mutateAsync, isPending } =
api.application.saveBuildType.useMutation();
const { data, refetch } = api.application.one.useQuery(
{ applicationId },
{ enabled: !!applicationId },
);
- const form = useForm({
+ const form = useForm({
defaultValues: {
buildType: BuildType.nixpacks,
},
@@ -347,7 +342,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
Docker File
@@ -533,7 +528,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
>
)}
-
+
Save
diff --git a/apps/dokploy/components/dashboard/application/deployments/cancel-queues.tsx b/apps/dokploy/components/dashboard/application/deployments/cancel-queues.tsx
index e957a496c..ed1373aa0 100644
--- a/apps/dokploy/components/dashboard/application/deployments/cancel-queues.tsx
+++ b/apps/dokploy/components/dashboard/application/deployments/cancel-queues.tsx
@@ -1,4 +1,4 @@
-import { Paintbrush } from "lucide-react";
+import { Ban } from "lucide-react";
import { toast } from "sonner";
import {
AlertDialog,
@@ -20,7 +20,7 @@ interface Props {
}
export const CancelQueues = ({ id, type }: Props) => {
- const { mutateAsync, isLoading } =
+ const { mutateAsync, isPending } =
type === "application"
? api.application.cleanQueues.useMutation()
: api.compose.cleanQueues.useMutation();
@@ -33,9 +33,9 @@ export const CancelQueues = ({ id, type }: Props) => {
return (
-
+
Cancel Queues
-
+
diff --git a/apps/dokploy/components/dashboard/application/deployments/clear-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/clear-deployments.tsx
new file mode 100644
index 000000000..81f998a9d
--- /dev/null
+++ b/apps/dokploy/components/dashboard/application/deployments/clear-deployments.tsx
@@ -0,0 +1,73 @@
+import { Paintbrush } from "lucide-react";
+import { toast } from "sonner";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from "@/components/ui/alert-dialog";
+import { Button } from "@/components/ui/button";
+import { api } from "@/utils/api";
+
+interface Props {
+ id: string;
+ type: "application" | "compose";
+}
+
+export const ClearDeployments = ({ id, type }: Props) => {
+ const utils = api.useUtils();
+ const { mutateAsync, isPending } =
+ type === "application"
+ ? api.application.clearDeployments.useMutation()
+ : api.compose.clearDeployments.useMutation();
+
+ return (
+
+
+
+ Clear deployments
+
+
+
+
+
+
+ Are you sure you want to clear old deployments?
+
+
+ This will delete all old deployment records and logs, keeping only
+ the active deployment (the most recent successful one).
+
+
+
+ Cancel
+ {
+ await mutateAsync({
+ applicationId: id || "",
+ composeId: id || "",
+ })
+ .then(async () => {
+ toast.success("Old deployments cleared successfully");
+ await utils.deployment.allByType.invalidate({
+ id,
+ type: type as "application" | "compose",
+ });
+ })
+ .catch((err) => {
+ toast.error(err.message);
+ });
+ }}
+ >
+ Confirm
+
+
+
+
+ );
+};
diff --git a/apps/dokploy/components/dashboard/application/deployments/kill-build.tsx b/apps/dokploy/components/dashboard/application/deployments/kill-build.tsx
index 784534dd6..ad5e9b058 100644
--- a/apps/dokploy/components/dashboard/application/deployments/kill-build.tsx
+++ b/apps/dokploy/components/dashboard/application/deployments/kill-build.tsx
@@ -20,7 +20,7 @@ interface Props {
}
export const KillBuild = ({ id, type }: Props) => {
- const { mutateAsync, isLoading } =
+ const { mutateAsync, isPending } =
type === "application"
? api.application.killBuild.useMutation()
: api.compose.killBuild.useMutation();
@@ -28,7 +28,7 @@ export const KillBuild = ({ id, type }: Props) => {
return (
-
+
Kill Build
diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx
index 0d403ecd2..4285f04c4 100644
--- a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx
+++ b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx
@@ -194,13 +194,21 @@ export const ShowDeployment = ({
{" "}
{filteredLogs.length > 0 ? (
filteredLogs.map((log: LogLine, index: number) => (
-
+
))
) : (
<>
{optionalErrors.length > 0 ? (
optionalErrors.map((log: LogLine, index: number) => (
-
+
))
) : (
diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx
index cfe747d27..61841e294 100644
--- a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx
+++ b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx
@@ -6,6 +6,7 @@ import {
RefreshCcw,
RocketIcon,
Settings,
+ Trash2,
} from "lucide-react";
import React, { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
@@ -25,6 +26,7 @@ import {
import { api, type RouterOutputs } from "@/utils/api";
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
import { CancelQueues } from "./cancel-queues";
+import { ClearDeployments } from "./clear-deployments";
import { KillBuild } from "./kill-build";
import { RefreshToken } from "./refresh-token";
import { ShowDeployment } from "./show-deployment";
@@ -59,7 +61,7 @@ export const ShowDeployments = ({
const [activeLog, setActiveLog] = useState<
RouterOutputs["deployment"]["all"][number] | null
>(null);
- const { data: deployments, isLoading: isLoadingDeployments } =
+ const { data: deployments, isPending: isLoadingDeployments } =
api.deployment.allByType.useQuery(
{
id,
@@ -73,19 +75,21 @@ export const ShowDeployments = ({
const { data: isCloud } = api.settings.isCloud.useQuery();
- const { mutateAsync: rollback, isLoading: isRollingBack } =
+ const { mutateAsync: rollback, isPending: isRollingBack } =
api.rollback.rollback.useMutation();
- const { mutateAsync: killProcess, isLoading: isKillingProcess } =
+ const { mutateAsync: killProcess, isPending: isKillingProcess } =
api.deployment.killProcess.useMutation();
+ const { mutateAsync: removeDeployment, isPending: isRemovingDeployment } =
+ api.deployment.removeDeployment.useMutation();
// Cancel deployment mutations
const {
mutateAsync: cancelApplicationDeployment,
- isLoading: isCancellingApp,
+ isPending: isCancellingApp,
} = api.application.cancelDeployment.useMutation();
const {
mutateAsync: cancelComposeDeployment,
- isLoading: isCancellingCompose,
+ isPending: isCancellingCompose,
} = api.compose.cancelDeployment.useMutation();
const [url, setUrl] = React.useState("");
@@ -144,6 +148,9 @@ export const ShowDeployments = ({
+ {(type === "application" || type === "compose") && (
+
+ )}
{(type === "application" || type === "compose") && (
)}
@@ -252,6 +259,8 @@ export const ShowDeployments = ({
const isExpanded = expandedDescriptions.has(
deployment.deploymentId,
);
+ const canDelete =
+ deployment.status === "done" || deployment.status === "error";
return (
+ {canDelete && (
+
{
+ try {
+ await removeDeployment({
+ deploymentId: deployment.deploymentId,
+ });
+ toast.success("Deployment deleted successfully");
+ } catch (error) {
+ toast.error("Error deleting deployment");
+ }
+ }}
+ >
+
+ Delete
+
+
+
+ )}
+
{deployment?.rollback &&
deployment.status === "done" &&
type === "application" && (
diff --git a/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx b/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx
index 6af0e1e8c..00eb62272 100644
--- a/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx
+++ b/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
@@ -159,11 +159,11 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
},
);
- const { mutateAsync, isError, error, isLoading } = domainId
+ const { mutateAsync, isError, error, isPending } = domainId
? api.domain.update.useMutation()
: api.domain.create.useMutation();
- const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
+ const { mutateAsync: generateDomain, isPending: isLoadingGenerate } =
api.domain.generateDomain.useMutation();
const { data: canGenerateTraefikMeDomains } =
@@ -240,7 +240,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
domainType: type,
});
}
- }, [form, data, isLoading, domainId]);
+ }, [form, data, isPending, domainId]);
// Separate effect for handling custom cert resolver validation
useEffect(() => {
@@ -730,7 +730,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
-
+
{dictionary.submit}
diff --git a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx
index 1fd3d82e9..c207ba59c 100644
--- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx
+++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx
@@ -97,7 +97,7 @@ export const ShowDomains = ({ id, type }: Props) => {
const { mutateAsync: validateDomain } =
api.domain.validateDomain.useMutation();
- const { mutateAsync: deleteDomain, isLoading: isRemoving } =
+ const { mutateAsync: deleteDomain, isPending: isRemoving } =
api.domain.delete.useMutation();
const handleValidateDomain = async (host: string) => {
diff --git a/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx b/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx
index 797a317a8..8ff0f6a63 100644
--- a/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx
+++ b/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { EyeIcon, EyeOffIcon } from "lucide-react";
import { type CSSProperties, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -60,7 +60,7 @@ export const ShowEnvironment = ({ id, type }: Props) => {
mongo: () => api.mongo.update.useMutation(),
compose: () => api.compose.update.useMutation(),
};
- const { mutateAsync, isLoading } = mutationMap[type]
+ const { mutateAsync, isPending } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();
@@ -111,7 +111,7 @@ export const ShowEnvironment = ({ id, type }: Props) => {
// Add keyboard shortcut for Ctrl+S/Cmd+S
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
- if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
+ if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isPending) {
e.preventDefault();
form.handleSubmit(onSubmit)();
}
@@ -121,7 +121,7 @@ export const ShowEnvironment = ({ id, type }: Props) => {
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
- }, [form, onSubmit, isLoading]);
+ }, [form, onSubmit, isPending]);
return (
@@ -196,7 +196,7 @@ PORT=3000
)}
{
- const { mutateAsync, isLoading } =
+ const { mutateAsync, isPending } =
api.application.saveEnvironment.useMutation();
const { data, refetch } = api.application.one.useQuery(
@@ -104,7 +104,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
// Add keyboard shortcut for Ctrl+S/Cmd+S
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
- if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
+ if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isPending) {
e.preventDefault();
form.handleSubmit(onSubmit)();
}
@@ -114,7 +114,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
- }, [form, onSubmit, isLoading]);
+ }, [form, onSubmit, isPending]);
return (
@@ -214,7 +214,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
)}
{
api.bitbucket.bitbucketProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
- const { mutateAsync, isLoading: isSavingBitbucketProvider } =
+ const { mutateAsync, isPending: isSavingBitbucketProvider } =
api.application.saveBitbucketProvider.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
buildPath: "/",
repository: {
@@ -245,13 +245,13 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {isLoadingRepositories
- ? "Loading...."
- : field.value.owner
- ? repositories?.find(
+ {!field.value.owner
+ ? "Select repository"
+ : isLoadingRepositories
+ ? "Loading...."
+ : (repositories?.find(
(repo) => repo.name === field.value.repo,
- )?.name
- : "Select repository"}
+ )?.name ?? "Select repository")}
@@ -263,11 +263,15 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
- {isLoadingRepositories && (
+ {!bitbucketId ? (
+
+ Select a Bitbucket account first
+
+ ) : isLoadingRepositories ? (
Loading Repositories....
- )}
+ ) : null}
No repositories found.
@@ -329,7 +333,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -346,7 +350,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
- {status === "loading" && fetchStatus === "fetching" && (
+ {status === "pending" && fetchStatus === "fetching" && (
Loading Branches....
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 fcdcf0a93..078271bca 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,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
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 00e18c2ab..583b865c5 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,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { TrashIcon } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
@@ -24,10 +24,10 @@ interface Props {
export const SaveDragNDrop = ({ applicationId }: Props) => {
const { data, refetch } = api.application.one.useQuery({ applicationId });
- const { mutateAsync, isLoading } =
+ const { mutateAsync, isPending } =
api.application.dropDeployment.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {},
resolver: zodResolver(uploadFileSchema),
});
@@ -129,8 +129,8 @@ export const SaveDragNDrop = ({ applicationId }: Props) => {
Deploy{" "}
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 e9be3a2f5..624adeb55 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,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -58,10 +58,10 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
const { data: sshKeys } = api.sshKey.all.useQuery();
const router = useRouter();
- const { mutateAsync, isLoading } =
+ const { mutateAsync, isPending } =
api.application.saveGitProvider.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
branch: "",
buildPath: "/",
@@ -317,7 +317,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
-
+
Save
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 2198f4a97..02cae2c4a 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,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -88,10 +88,10 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
- const { mutateAsync, isLoading: isSavingGiteaProvider } =
+ const { mutateAsync, isPending: isSavingGiteaProvider } =
api.application.saveGiteaProvider.useMutation();
- const form = useForm
({
+ const form = useForm({
defaultValues: {
buildPath: "/",
repository: {
@@ -258,14 +258,14 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {isLoadingRepositories
- ? "Loading...."
- : field.value.owner
- ? repositories?.find(
+ {!field.value.owner
+ ? "Select repository"
+ : isLoadingRepositories
+ ? "Loading...."
+ : (repositories?.find(
(repo: GiteaRepository) =>
repo.name === field.value.repo,
- )?.name
- : "Select repository"}
+ )?.name ?? "Select repository")}
@@ -277,11 +277,15 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
- {isLoadingRepositories && (
+ {!giteaId ? (
+
+ Select a Gitea account first
+
+ ) : isLoadingRepositories ? (
Loading Repositories....
- )}
+ ) : null}
No repositories found.
@@ -349,7 +353,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -367,7 +371,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
- {status === "loading" && fetchStatus === "fetching" && (
+ {status === "pending" && fetchStatus === "fetching" && (
Loading Branches....
@@ -459,7 +463,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
{
- const newPaths = [...field.value];
+ const newPaths = [...(field.value || [])];
newPaths.splice(index, 1);
field.onChange(newPaths);
}}
@@ -477,7 +481,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
const input = e.currentTarget;
const path = input.value.trim();
if (path) {
- field.onChange([...field.value, path]);
+ field.onChange([...(field.value || []), path]);
input.value = "";
}
}
@@ -494,7 +498,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
) as HTMLInputElement;
const path = input.value.trim();
if (path) {
- field.onChange([...field.value, path]);
+ field.onChange([...(field.value || []), path]);
input.value = "";
}
}}
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 80d6850ca..6bce2d243 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,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -72,10 +72,10 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
const { data: githubProviders } = api.github.githubProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
- const { mutateAsync, isLoading: isSavingGithubProvider } =
+ const { mutateAsync, isPending: isSavingGithubProvider } =
api.application.saveGithubProvider.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
buildPath: "/",
repository: {
@@ -94,7 +94,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
const githubId = form.watch("githubId");
const triggerType = form.watch("triggerType");
- const { data: repositories, isLoading: isLoadingRepositories } =
+ const { data: repositories, isPending: isLoadingRepositories } =
api.github.getGithubRepositories.useQuery(
{
githubId,
@@ -233,13 +233,13 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {isLoadingRepositories
- ? "Loading...."
- : field.value.owner
- ? repositories?.find(
+ {!field.value.owner
+ ? "Select repository"
+ : isLoadingRepositories
+ ? "Loading...."
+ : (repositories?.find(
(repo) => repo.name === field.value.repo,
- )?.name
- : "Select repository"}
+ )?.name ?? "Select repository")}
@@ -251,11 +251,15 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
- {isLoadingRepositories && (
+ {!githubId ? (
+
+ Select a GitHub account first
+
+ ) : isLoadingRepositories ? (
Loading Repositories....
- )}
+ ) : null}
No repositories found.
@@ -316,7 +320,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -333,7 +337,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
- {status === "loading" && fetchStatus === "fetching" && (
+ {status === "pending" && fetchStatus === "fetching" && (
Loading Branches....
@@ -455,7 +459,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
{field.value?.map((path, index) => (
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 6197fc49f..b49a1658f 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,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect, useMemo } from "react";
@@ -74,10 +74,10 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
- const { mutateAsync, isLoading: isSavingGitlabProvider } =
+ const { mutateAsync, isPending: isSavingGitlabProvider } =
api.application.saveGitlabProvider.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
buildPath: "/",
repository: {
@@ -254,13 +254,13 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {isLoadingRepositories
- ? "Loading...."
- : field.value.owner
- ? repositories?.find(
+ {!field.value.owner
+ ? "Select repository"
+ : isLoadingRepositories
+ ? "Loading...."
+ : (repositories?.find(
(repo) => repo.name === field.value.repo,
- )?.name
- : "Select repository"}
+ )?.name ?? "Select repository")}
@@ -272,11 +272,15 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
- {isLoadingRepositories && (
+ {!gitlabId ? (
+
+ Select a GitLab account first
+
+ ) : isLoadingRepositories ? (
Loading Repositories....
- )}
+ ) : null}
No repositories found.
@@ -347,7 +351,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -364,7 +368,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
- {status === "loading" && fetchStatus === "fetching" && (
+ {status === "pending" && fetchStatus === "fetching" && (
Loading Branches....
@@ -444,7 +448,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
{field.value?.map((path, index) => (
diff --git a/apps/dokploy/components/dashboard/application/general/generic/show.tsx b/apps/dokploy/components/dashboard/application/general/generic/show.tsx
index a60db800c..9a49b204e 100644
--- a/apps/dokploy/components/dashboard/application/general/generic/show.tsx
+++ b/apps/dokploy/components/dashboard/application/general/generic/show.tsx
@@ -36,13 +36,13 @@ interface Props {
}
export const ShowProviderForm = ({ applicationId }: Props) => {
- const { data: githubProviders, isLoading: isLoadingGithub } =
+ const { data: githubProviders, isPending: isLoadingGithub } =
api.github.githubProviders.useQuery();
- const { data: gitlabProviders, isLoading: isLoadingGitlab } =
+ const { data: gitlabProviders, isPending: isLoadingGitlab } =
api.gitlab.gitlabProviders.useQuery();
- const { data: bitbucketProviders, isLoading: isLoadingBitbucket } =
+ const { data: bitbucketProviders, isPending: isLoadingBitbucket } =
api.bitbucket.bitbucketProviders.useQuery();
- const { data: giteaProviders, isLoading: isLoadingGitea } =
+ const { data: giteaProviders, isPending: isLoadingGitea } =
api.gitea.giteaProviders.useQuery();
const { data: application, refetch } = api.application.one.useQuery({
diff --git a/apps/dokploy/components/dashboard/application/general/show.tsx b/apps/dokploy/components/dashboard/application/general/show.tsx
index 5387659ad..ee42caa5e 100644
--- a/apps/dokploy/components/dashboard/application/general/show.tsx
+++ b/apps/dokploy/components/dashboard/application/general/show.tsx
@@ -37,14 +37,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
{ enabled: !!applicationId },
);
const { mutateAsync: update } = api.application.update.useMutation();
- const { mutateAsync: start, isLoading: isStarting } =
+ const { mutateAsync: start, isPending: isStarting } =
api.application.start.useMutation();
- const { mutateAsync: stop, isLoading: isStopping } =
+ const { mutateAsync: stop, isPending: isStopping } =
api.application.stop.useMutation();
const { mutateAsync: deploy } = api.application.deploy.useMutation();
- const { mutateAsync: reload, isLoading: isReloading } =
+ const { mutateAsync: reload, isPending: isReloading } =
api.application.reload.useMutation();
const { mutateAsync: redeploy } = api.application.redeploy.useMutation();
diff --git a/apps/dokploy/components/dashboard/application/logs/show.tsx b/apps/dokploy/components/dashboard/application/logs/show.tsx
index 941ddef50..cbb6bce09 100644
--- a/apps/dokploy/components/dashboard/application/logs/show.tsx
+++ b/apps/dokploy/components/dashboard/application/logs/show.tsx
@@ -56,7 +56,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
const [containerId, setContainerId] = useState();
const [option, setOption] = useState<"swarm" | "native">("native");
- const { data: services, isLoading: servicesLoading } =
+ const { data: services, isPending: servicesLoading } =
api.docker.getServiceContainersByAppName.useQuery(
{
appName,
@@ -67,7 +67,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
},
);
- const { data: containers, isLoading: containersLoading } =
+ const { data: containers, isPending: containersLoading } =
api.docker.getContainersByAppNameMatch.useQuery(
{
appName,
diff --git a/apps/dokploy/components/dashboard/application/patches/create-file-dialog.tsx b/apps/dokploy/components/dashboard/application/patches/create-file-dialog.tsx
new file mode 100644
index 000000000..5f6f88e36
--- /dev/null
+++ b/apps/dokploy/components/dashboard/application/patches/create-file-dialog.tsx
@@ -0,0 +1,107 @@
+import { FilePlus } from "lucide-react";
+import { useState } from "react";
+import { CodeEditor } from "@/components/shared/code-editor";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+
+interface Props {
+ folderPath: string;
+ onCreate: (filename: string, content: string) => void;
+ onOpenChange: (open: boolean) => void;
+ alwaysVisible?: boolean;
+}
+
+export const CreateFileDialog = ({
+ folderPath,
+ onCreate,
+ onOpenChange,
+ alwaysVisible = false,
+}: Props) => {
+ const [filename, setFilename] = useState("");
+ const [content, setContent] = useState("");
+
+ const handleCreate = () => {
+ if (!filename.trim()) return;
+ onCreate(filename.trim(), content);
+ setFilename("");
+ setContent("");
+ onOpenChange(false);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/dokploy/components/dashboard/application/patches/edit-patch-dialog.tsx b/apps/dokploy/components/dashboard/application/patches/edit-patch-dialog.tsx
new file mode 100644
index 000000000..8c5a42836
--- /dev/null
+++ b/apps/dokploy/components/dashboard/application/patches/edit-patch-dialog.tsx
@@ -0,0 +1,102 @@
+import { Loader2, Pencil } from "lucide-react";
+import { useEffect, useState } from "react";
+import { toast } from "sonner";
+import { CodeEditor } from "@/components/shared/code-editor";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { api } from "@/utils/api";
+
+interface Props {
+ patchId: string;
+ entityId: string;
+ type: "application" | "compose";
+ onSuccess?: () => void;
+}
+
+export const EditPatchDialog = ({
+ patchId,
+ entityId,
+ type,
+ onSuccess,
+}: Props) => {
+ const { data: patch, isPending: isPatchLoading } = api.patch.one.useQuery(
+ { patchId },
+ { enabled: !!patchId },
+ );
+ const [content, setContent] = useState("");
+
+ useEffect(() => {
+ if (patch) {
+ setContent(patch.content);
+ }
+ }, [patch]);
+
+ const utils = api.useUtils();
+ const updatePatch = api.patch.update.useMutation();
+
+ const handleSave = () => {
+ updatePatch
+ .mutateAsync({ patchId, content })
+ .then(() => {
+ toast.success("Patch saved");
+ utils.patch.byEntityId.invalidate({ id: entityId, type });
+ onSuccess?.();
+ })
+ .catch((err) => {
+ toast.error(err.message);
+ });
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ Edit Patch
+
+ {patch ? `Editing: ${patch.filePath}` : "Loading patch..."}
+
+
+ {isPatchLoading ? (
+
+
+
+ ) : (
+
+ setContent(value ?? "")}
+ className="h-[400px] w-full"
+ wrapperClassName="h-[400px]"
+ lineWrapping
+ />
+
+ )}
+
+
+ Cancel
+
+
+ {updatePatch.isPending && (
+
+ )}
+ Save
+
+
+
+
+ );
+};
diff --git a/apps/dokploy/components/dashboard/application/patches/index.ts b/apps/dokploy/components/dashboard/application/patches/index.ts
new file mode 100644
index 000000000..1854bd3e5
--- /dev/null
+++ b/apps/dokploy/components/dashboard/application/patches/index.ts
@@ -0,0 +1,2 @@
+export * from "./show-patches";
+export * from "./patch-editor";
diff --git a/apps/dokploy/components/dashboard/application/patches/patch-editor.tsx b/apps/dokploy/components/dashboard/application/patches/patch-editor.tsx
new file mode 100644
index 000000000..4b212b004
--- /dev/null
+++ b/apps/dokploy/components/dashboard/application/patches/patch-editor.tsx
@@ -0,0 +1,368 @@
+import {
+ ArrowLeft,
+ ChevronRight,
+ File,
+ Folder,
+ Loader2,
+ Save,
+ Trash2,
+} from "lucide-react";
+import { useCallback, useEffect, useState } from "react";
+import { toast } from "sonner";
+import { CodeEditor } from "@/components/shared/code-editor";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { api } from "@/utils/api";
+import { CreateFileDialog } from "./create-file-dialog";
+
+interface Props {
+ id: string;
+ type: "application" | "compose";
+ repoPath: string;
+ onClose: () => void;
+}
+
+type DirectoryEntry = {
+ name: string;
+ path: string;
+ type: "file" | "directory";
+ children?: DirectoryEntry[];
+};
+
+export const PatchEditor = ({ id, type, repoPath, onClose }: Props) => {
+ const [selectedFile, setSelectedFile] = useState(null);
+ const [fileContent, setFileContent] = useState("");
+ const [createFolderPath, setCreateFolderPath] = useState(null);
+ const [expandedFolders, setExpandedFolders] = useState>(
+ new Set(),
+ );
+
+ const utils = api.useUtils();
+ const { data: directories, isPending: isDirLoading } =
+ api.patch.readRepoDirectories.useQuery(
+ { id: id, type, repoPath },
+ { enabled: !!repoPath },
+ );
+
+ const { data: patches } = api.patch.byEntityId.useQuery(
+ { id, type },
+ { enabled: !!id },
+ );
+
+ const { mutateAsync: saveAsPatch, isPending: isSavingPatch } =
+ api.patch.saveFileAsPatch.useMutation();
+
+ const { mutateAsync: markForDeletion, isPending: isMarkingDeletion } =
+ api.patch.markFileForDeletion.useMutation();
+
+ const updatePatch = api.patch.update.useMutation();
+
+ const { data: fileData, isFetching: isFileLoading } =
+ api.patch.readRepoFile.useQuery(
+ {
+ id,
+ type,
+ filePath: selectedFile || "",
+ },
+ {
+ enabled: !!selectedFile,
+ },
+ );
+
+ useEffect(() => {
+ if (fileData !== undefined) {
+ setFileContent(fileData);
+ }
+ }, [fileData]);
+
+ const handleFileSelect = (filePath: string) => {
+ setSelectedFile(filePath);
+ };
+
+ const toggleFolder = (path: string) => {
+ setExpandedFolders((prev) => {
+ const next = new Set(prev);
+ if (next.has(path)) {
+ next.delete(path);
+ } else {
+ next.add(path);
+ }
+ return next;
+ });
+ };
+
+ const handleSave = () => {
+ if (!selectedFile) return;
+ saveAsPatch({
+ id,
+ type,
+ filePath: selectedFile,
+ content: fileContent,
+ patchType: "update",
+ })
+ .then(() => {
+ toast.success("Patch saved");
+ utils.patch.byEntityId.invalidate({ id, type });
+ })
+ .catch(() => {
+ toast.error("Failed to save patch");
+ });
+ };
+
+ const handleMarkForDeletion = () => {
+ if (!selectedFile) return;
+ markForDeletion({ id, type, filePath: selectedFile })
+ .then(() => {
+ toast.success("File marked for deletion");
+ utils.patch.byEntityId.invalidate({ id, type });
+ })
+ .catch(() => {
+ toast.error("Failed to mark file for deletion");
+ });
+ };
+
+ const handleCreateFile = useCallback(
+ (folderPath: string, filename: string, content: string) => {
+ const filePath = folderPath ? `${folderPath}/${filename}` : filename;
+ saveAsPatch({
+ id,
+ type,
+ filePath,
+ content,
+ patchType: "create",
+ })
+ .then(() => {
+ toast.success("File created");
+ utils.patch.byEntityId.invalidate({ id, type });
+ })
+ .catch(() => {
+ toast.error("Failed to create file");
+ });
+ },
+ [id, type, saveAsPatch, utils],
+ );
+
+ const selectedFilePatch = patches?.find(
+ (p) => p.filePath === selectedFile && p.type === "delete",
+ );
+
+ const handleUnmarkDeletion = () => {
+ if (!selectedFilePatch) return;
+ updatePatch
+ .mutateAsync({
+ patchId: selectedFilePatch.patchId,
+ type: "update",
+ content: fileData || "",
+ })
+ .then(() => {
+ toast.success("Deletion unmarked");
+ utils.patch.byEntityId.invalidate({ id, type });
+ })
+ .catch(() => {
+ toast.error("Failed to unmark deletion");
+ });
+ };
+
+ const hasChanges = fileData !== undefined && fileContent !== fileData;
+
+ const renderTree = useCallback(
+ (entries: DirectoryEntry[], depth = 0) => {
+ return entries
+ .sort((a, b) => {
+ // Directories first, then alphabetically
+ if (a.type !== b.type) {
+ return a.type === "directory" ? -1 : 1;
+ }
+ return a.name.localeCompare(b.name);
+ })
+ .map((entry) => {
+ const isExpanded = expandedFolders.has(entry.path);
+ const isSelected = selectedFile === entry.path;
+
+ if (entry.type === "directory") {
+ return (
+
+
+ toggleFolder(entry.path)}
+ className={
+ "flex-1 flex items-center gap-2 px-2 py-1.5 text-sm hover:bg-muted/50 rounded-md transition-colors text-left min-w-0"
+ }
+ style={{ paddingLeft: `${depth * 12 + 8}px` }}
+ >
+
+
+ {entry.name}
+
+
+ handleCreateFile(entry.path, filename, content)
+ }
+ onOpenChange={(open) =>
+ setCreateFolderPath(open ? entry.path : null)
+ }
+ />
+
+ {isExpanded && entry.children && (
+
{renderTree(entry.children, depth + 1)}
+ )}
+
+ );
+ }
+
+ const isMarkedForDeletion = patches?.some(
+ (p) => p.filePath === entry.path && p.type === "delete",
+ );
+
+ return (
+ handleFileSelect(entry.path)}
+ className={`w-full flex items-center gap-2 px-2 py-1.5 text-sm hover:bg-muted/50 rounded-md transition-colors ${
+ isSelected ? "bg-muted" : ""
+ } ${isMarkedForDeletion ? "text-destructive" : ""}`}
+ style={{ paddingLeft: `${depth * 12 + 28}px` }}
+ >
+
+ {entry.name}
+ {isMarkedForDeletion && (
+
+ )}
+
+ );
+ });
+ },
+ [expandedFolders, selectedFile, patches, handleCreateFile],
+ );
+
+ return (
+
+
+
+
+
+
+
+ Edit File
+
+ {selectedFile
+ ? `Editing: ${selectedFile}`
+ : "Select a file from the tree to edit"}
+
+
+
+ {selectedFile && (
+
+ {selectedFilePatch ? (
+
+ {updatePatch.isPending && (
+
+ )}
+ Unmark deletion
+
+ ) : (
+ <>
+
+ {isMarkingDeletion && (
+
+ )}
+
+ Mark for deletion
+
+
+ {isSavingPatch && (
+
+ )}
+
+ Save Patch
+
+ >
+ )}
+
+ )}
+
+
+
+
+
+
+
+
+ handleCreateFile("", filename, content)
+ }
+ onOpenChange={(open) =>
+ setCreateFolderPath(open ? "" : null)
+ }
+ />
+
+ New file in root
+
+
+ {isDirLoading ? (
+
+
+
+ ) : directories ? (
+ renderTree(directories)
+ ) : (
+
+ No files found
+
+ )}
+
+
+
+
+ {isFileLoading ? (
+
+
+
+ ) : selectedFile ? (
+
setFileContent(value || "")}
+ className="h-full w-full"
+ wrapperClassName="h-full"
+ lineWrapping
+ />
+ ) : (
+
+ Select a file to edit
+
+ )}
+
+
+
+
+ );
+};
diff --git a/apps/dokploy/components/dashboard/application/patches/show-patches.tsx b/apps/dokploy/components/dashboard/application/patches/show-patches.tsx
new file mode 100644
index 000000000..e471b3fc1
--- /dev/null
+++ b/apps/dokploy/components/dashboard/application/patches/show-patches.tsx
@@ -0,0 +1,225 @@
+import { File, FilePlus2, Loader2, Trash2 } from "lucide-react";
+import { useState } from "react";
+import { toast } from "sonner";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Switch } from "@/components/ui/switch";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { api } from "@/utils/api";
+import { EditPatchDialog } from "./edit-patch-dialog";
+import { PatchEditor } from "./patch-editor";
+
+interface Props {
+ id: string;
+ type: "application" | "compose";
+}
+
+export const ShowPatches = ({ id, type }: Props) => {
+ const [selectedFile, setSelectedFile] = useState(null);
+ const [repoPath, setRepoPath] = useState(null);
+ const [isLoadingRepo, setIsLoadingRepo] = useState(false);
+
+ const utils = api.useUtils();
+
+ const { data: patches, isPending: isPatchesLoading } =
+ api.patch.byEntityId.useQuery({ id, type }, { enabled: !!id });
+
+ const mutationMap = {
+ application: () => api.patch.delete.useMutation(),
+ compose: () => api.patch.delete.useMutation(),
+ };
+
+ const ensureRepo = api.patch.ensureRepo.useMutation();
+
+ const togglePatch = api.patch.toggleEnabled.useMutation();
+
+ const { mutateAsync } = mutationMap[type]
+ ? mutationMap[type]()
+ : api.patch.delete.useMutation();
+
+ const handleCloseEditor = () => {
+ setSelectedFile(null);
+ setRepoPath(null);
+ };
+
+ if (repoPath) {
+ return (
+
+ );
+ }
+
+ const handleOpenEditor = async () => {
+ setIsLoadingRepo(true);
+ await ensureRepo
+ .mutateAsync({ id, type })
+ .then((result) => {
+ setRepoPath(result);
+ })
+ .catch((err) => {
+ toast.error(err.message);
+ })
+ .finally(() => {
+ setIsLoadingRepo(false);
+ });
+ };
+
+ return (
+
+
+
+ Patches
+
+ Apply code patches to your repository during build. Patches are
+ applied after cloning the repository and before building.
+
+
+ {patches && patches?.length > 0 && (
+
+ {isLoadingRepo && }
+
+ Create Patch
+
+ )}
+
+
+ {isPatchesLoading ? (
+
+
+
+ ) : patches?.length === 0 ? (
+
+
+
+
+
+
No patches yet
+
+ Add file patches to modify your repo before each build—configs,
+ env, or code. Create your first patch to get started.
+
+
+
+ {isLoadingRepo && (
+
+ )}
+
+ Create Patch
+
+
+ ) : (
+
+
+
+ File Path
+ Type
+ Enabled
+ Actions
+
+
+
+ {patches?.map((patch) => (
+
+
+
+
+ {patch.filePath}
+
+
+
+
+ {patch.type}
+
+
+
+ {
+ togglePatch
+ .mutateAsync({
+ patchId: patch.patchId,
+ enabled: checked,
+ })
+ .then(() => {
+ toast.success("Patch updated");
+ utils.patch.byEntityId.invalidate({
+ id,
+ type,
+ });
+ })
+ .catch((err) => {
+ toast.error(err.message);
+ })
+ .finally(() => {
+ setIsLoadingRepo(false);
+ });
+ }}
+ />
+
+
+
+ {(patch.type === "update" || patch.type === "create") && (
+
+ )}
+ {
+ mutateAsync({ patchId: patch.patchId })
+ .then(() => {
+ toast.success("Patch deleted");
+ utils.patch.byEntityId.invalidate({
+ id,
+ type,
+ });
+ })
+ .catch((err) => {
+ toast.error(err.message);
+ });
+ }}
+ title="Delete patch"
+ >
+
+
+
+
+
+ ))}
+
+
+ )}
+
+
+ );
+};
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 bb9321a51..72815fd8f 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,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Dices } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -75,11 +75,11 @@ export const AddPreviewDomain = ({
},
);
- const { mutateAsync, isError, error, isLoading } = domainId
+ const { mutateAsync, isError, error, isPending } = domainId
? api.domain.update.useMutation()
: api.domain.create.useMutation();
- const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
+ const { mutateAsync: generateDomain, isPending: isLoadingGenerate } =
api.domain.generateDomain.useMutation();
const form = useForm({
@@ -103,7 +103,7 @@ export const AddPreviewDomain = ({
if (!domainId) {
form.reset({});
}
- }, [form, form.reset, data, isLoading]);
+ }, [form, form.reset, data, isPending]);
const dictionary = {
success: domainId ? "Domain Updated" : "Domain Created",
@@ -301,7 +301,7 @@ export const AddPreviewDomain = ({
-
+
{dictionary.submit}
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 6cf8d8830..e12400a7c 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
@@ -43,7 +43,7 @@ interface Props {
export const ShowPreviewDeployments = ({ applicationId }: Props) => {
const { data } = api.application.one.useQuery({ applicationId });
- const { mutateAsync: deletePreviewDeployment, isLoading } =
+ const { mutateAsync: deletePreviewDeployment, isPending } =
api.previewDeployment.delete.useMutation();
const { mutateAsync: redeployPreviewDeployment } =
@@ -57,8 +57,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
{ applicationId },
{
enabled: !!applicationId,
- refetchInterval: (data) =>
- data?.some((d) => d.previewStatus === "running") ? 2000 : false,
+ refetchInterval: 2000,
},
);
@@ -282,7 +281,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
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 f8e6fab68..d2840cd67 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,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { HelpCircle, Plus, Settings2, X } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -80,7 +80,7 @@ interface Props {
export const ShowPreviewSettings = ({ applicationId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [isEnabled, setIsEnabled] = useState(false);
- const { mutateAsync: updateApplication, isLoading } =
+ const { mutateAsync: updateApplication, isPending } =
api.application.update.useMutation();
const { data, refetch } = api.application.one.useQuery({ applicationId });
@@ -535,7 +535,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
Cancel
diff --git a/apps/dokploy/components/dashboard/application/rollbacks/show-rollback-settings.tsx b/apps/dokploy/components/dashboard/application/rollbacks/show-rollback-settings.tsx
index a06cf5697..b119aa778 100644
--- a/apps/dokploy/components/dashboard/application/rollbacks/show-rollback-settings.tsx
+++ b/apps/dokploy/components/dashboard/application/rollbacks/show-rollback-settings.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -71,7 +71,7 @@ export const ShowRollbackSettings = ({ applicationId, children }: Props) => {
},
);
- const { mutateAsync: updateApplication, isLoading } =
+ const { mutateAsync: updateApplication, isPending } =
api.application.update.useMutation();
const { data: registries } = api.registry.all.useQuery();
@@ -212,7 +212,7 @@ export const ShowRollbackSettings = ({ applicationId, children }: Props) => {
/>
)}
-
+
Save Settings
diff --git a/apps/dokploy/components/dashboard/application/schedules/handle-schedules.tsx b/apps/dokploy/components/dashboard/application/schedules/handle-schedules.tsx
index e85b1b004..36ddb53f1 100644
--- a/apps/dokploy/components/dashboard/application/schedules/handle-schedules.tsx
+++ b/apps/dokploy/components/dashboard/application/schedules/handle-schedules.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
import {
CheckIcon,
ChevronsUpDown,
@@ -220,8 +220,8 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [cacheType, setCacheType] = useState("cache");
const utils = api.useUtils();
- const form = useForm>({
- resolver: zodResolver(formSchema),
+ const form = useForm({
+ resolver: standardSchemaResolver(formSchema),
defaultValues: {
name: "",
cronExpression: "",
@@ -275,11 +275,11 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
}
}, [form, schedule, scheduleId]);
- const { mutateAsync, isLoading } = scheduleId
+ const { mutateAsync, isPending } = scheduleId
? api.schedule.update.useMutation()
: api.schedule.create.useMutation();
- const onSubmit = async (values: z.infer) => {
+ const onSubmit = async (values: z.output) => {
if (!id && !scheduleId) return;
await mutateAsync({
@@ -662,7 +662,7 @@ echo "Hello, world!"
)}
/>
-
+
{scheduleId ? "Update" : "Create"} Schedule
diff --git a/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx b/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx
index 26bfa9421..a9550fda2 100644
--- a/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx
+++ b/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx
@@ -51,7 +51,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
},
);
const utils = api.useUtils();
- const { mutateAsync: deleteSchedule, isLoading: isDeleting } =
+ const { mutateAsync: deleteSchedule, isPending: isDeleting } =
api.schedule.delete.useMutation();
const { mutateAsync: runManually } = api.schedule.runManually.useMutation();
diff --git a/apps/dokploy/components/dashboard/application/update-application.tsx b/apps/dokploy/components/dashboard/application/update-application.tsx
index 754074d75..98c49a999 100644
--- a/apps/dokploy/components/dashboard/application/update-application.tsx
+++ b/apps/dokploy/components/dashboard/application/update-application.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -43,7 +43,7 @@ interface Props {
export const UpdateApplication = ({ applicationId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
- const { mutateAsync, error, isError, isLoading } =
+ const { mutateAsync, error, isError, isPending } =
api.application.update.useMutation();
const { data } = api.application.one.useQuery(
{
@@ -148,7 +148,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
/>
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 e179713de..d0df60098 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,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { DatabaseZap, PenBoxIcon, PlusCircle, RefreshCw } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -116,7 +116,7 @@ export const HandleVolumeBackups = ({
const [keepLatestCountInput, setKeepLatestCountInput] = useState("");
const utils = api.useUtils();
- const form = useForm>({
+ const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
@@ -195,7 +195,7 @@ export const HandleVolumeBackups = ({
}
}, [form, volumeBackup, volumeBackupId]);
- const { mutateAsync, isLoading } = volumeBackupId
+ const { mutateAsync, isPending } = volumeBackupId
? api.volumeBackups.update.useMutation()
: api.volumeBackups.create.useMutation();
@@ -207,7 +207,7 @@ export const HandleVolumeBackups = ({
await mutateAsync({
...values,
- keepLatestCount: preparedKeepLatestCount,
+ keepLatestCount: preparedKeepLatestCount ?? undefined,
destinationId: values.destinationId,
volumeBackupId: volumeBackupId || "",
serviceType: volumeBackupType,
@@ -630,7 +630,7 @@ export const HandleVolumeBackups = ({
)}
/>
-
+
{volumeBackupId ? "Update" : "Create"} Volume Backup
diff --git a/apps/dokploy/components/dashboard/application/volume-backups/restore-volume-backups.tsx b/apps/dokploy/components/dashboard/application/volume-backups/restore-volume-backups.tsx
index 6eda33648..684620947 100644
--- a/apps/dokploy/components/dashboard/application/volume-backups/restore-volume-backups.tsx
+++ b/apps/dokploy/components/dashboard/application/volume-backups/restore-volume-backups.tsx
@@ -1,6 +1,6 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import copy from "copy-to-clipboard";
-import { debounce } from "lodash";
+import debounce from "lodash/debounce";
import { CheckIcon, ChevronsUpDown, Copy, RotateCcw } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -53,27 +53,15 @@ interface Props {
}
const RestoreBackupSchema = z.object({
- destinationId: z
- .string({
- required_error: "Please select a destination",
- })
- .min(1, {
- message: "Destination is required",
- }),
- backupFile: z
- .string({
- required_error: "Please select a backup file",
- })
- .min(1, {
- message: "Backup file is required",
- }),
- volumeName: z
- .string({
- required_error: "Please enter a volume name",
- })
- .min(1, {
- message: "Volume name is required",
- }),
+ destinationId: z.string().min(1, {
+ message: "Destination is required",
+ }),
+ backupFile: z.string().min(1, {
+ message: "Backup file is required",
+ }),
+ volumeName: z.string().min(1, {
+ message: "Volume name is required",
+ }),
});
export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
@@ -83,7 +71,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
const { data: destinations = [] } = api.destination.all.useQuery();
- const form = useForm>({
+ const form = useForm({
defaultValues: {
destinationId: "",
backupFile: "",
@@ -105,7 +93,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
debouncedSetSearch(value);
};
- const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
+ const { data: files = [], isPending } = api.backup.listBackupFiles.useQuery(
{
destinationId: destinationId,
search: debouncedSearchTerm,
@@ -294,7 +282,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
onValueChange={handleSearchChange}
className="h-9"
/>
- {isLoading ? (
+ {isPending ? (
Loading backup files...
diff --git a/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx b/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx
index 2e4dac472..526bcfa77 100644
--- a/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx
+++ b/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx
@@ -54,7 +54,7 @@ export const ShowVolumeBackups = ({
},
);
const utils = api.useUtils();
- const { mutateAsync: deleteVolumeBackup, isLoading: isDeleting } =
+ const { mutateAsync: deleteVolumeBackup, isPending: isDeleting } =
api.volumeBackups.delete.useMutation();
const { mutateAsync: runManually } =
api.volumeBackups.runManually.useMutation();
diff --git a/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx b/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx
index 52eb18907..c5f9334ec 100644
--- a/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx
+++ b/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -52,7 +52,7 @@ export const AddCommandCompose = ({ composeId }: Props) => {
const utils = api.useUtils();
- const { mutateAsync, isLoading } = api.compose.update.useMutation();
+ const { mutateAsync, isPending } = api.compose.update.useMutation();
const form = useForm({
defaultValues: {
@@ -128,7 +128,7 @@ export const AddCommandCompose = ({ composeId }: Props) => {
/>
-
+
Save
diff --git a/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx b/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx
index 5b6e04154..0fad7d20e 100644
--- a/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx
+++ b/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { AlertTriangle, Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
diff --git a/apps/dokploy/components/dashboard/compose/delete-service.tsx b/apps/dokploy/components/dashboard/compose/delete-service.tsx
index 5c8577dff..9d417ee91 100644
--- a/apps/dokploy/components/dashboard/compose/delete-service.tsx
+++ b/apps/dokploy/components/dashboard/compose/delete-service.tsx
@@ -1,5 +1,5 @@
import type { ServiceType } from "@dokploy/server/db/schema";
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import copy from "copy-to-clipboard";
import { Copy, Trash2 } from "lucide-react";
import { useRouter } from "next/router";
@@ -74,7 +74,7 @@ export const DeleteService = ({ id, type }: Props) => {
mongo: () => api.mongo.remove.useMutation(),
compose: () => api.compose.delete.useMutation(),
};
- const { mutateAsync, isLoading } = mutationMap[type]
+ const { mutateAsync, isPending } = mutationMap[type]
? mutationMap[type]()
: api.mongo.remove.useMutation();
const { push } = useRouter();
@@ -130,7 +130,7 @@ export const DeleteService = ({ id, type }: Props) => {
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
- isLoading={isLoading}
+ isLoading={isPending}
>
@@ -228,7 +228,7 @@ export const DeleteService = ({ id, type }: Props) => {
{
const { mutateAsync: update } = api.compose.update.useMutation();
const { mutateAsync: deploy } = api.compose.deploy.useMutation();
const { mutateAsync: redeploy } = api.compose.redeploy.useMutation();
- const { mutateAsync: start, isLoading: isStarting } =
+ const { mutateAsync: start, isPending: isStarting } =
api.compose.start.useMutation();
- const { mutateAsync: stop, isLoading: isStopping } =
+ const { mutateAsync: stop, isPending: isStopping } =
api.compose.stop.useMutation();
return (
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 cb727e2a9..8193ec8b6 100644
--- a/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -34,7 +34,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
{ enabled: !!composeId },
);
- const { mutateAsync, isLoading } = api.compose.update.useMutation();
+ const { mutateAsync, isPending } = api.compose.update.useMutation();
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const form = useForm
({
@@ -93,7 +93,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
// Add keyboard shortcut for Ctrl+S/Cmd+S
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
- if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
+ if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isPending) {
e.preventDefault();
form.handleSubmit(onSubmit)();
}
@@ -103,7 +103,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
- }, [form, onSubmit, isLoading]);
+ }, [form, onSubmit, isPending]);
return (
<>
@@ -167,7 +167,7 @@ services:
Save
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx
index c89b9893e..3e099251e 100644
--- a/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -74,10 +74,10 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
api.bitbucket.bitbucketProviders.useQuery();
const { data, refetch } = api.compose.one.useQuery({ composeId });
- const { mutateAsync, isLoading: isSavingBitbucketProvider } =
+ const { mutateAsync, isPending: isSavingBitbucketProvider } =
api.compose.update.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
composePath: "./docker-compose.yml",
repository: {
@@ -247,13 +247,13 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {isLoadingRepositories
- ? "Loading...."
- : field.value.owner
- ? repositories?.find(
+ {!field.value.owner
+ ? "Select repository"
+ : isLoadingRepositories
+ ? "Loading...."
+ : (repositories?.find(
(repo) => repo.name === field.value.repo,
- )?.name
- : "Select repository"}
+ )?.name ?? "Select repository")}
@@ -265,11 +265,15 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
- {isLoadingRepositories && (
+ {!bitbucketId ? (
+
+ Select a Bitbucket account first
+
+ ) : isLoadingRepositories ? (
Loading Repositories....
- )}
+ ) : null}
No repositories found.
@@ -331,7 +335,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -348,7 +352,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
- {status === "loading" && fetchStatus === "fetching" && (
+ {status === "pending" && fetchStatus === "fetching" && (
Loading Branches....
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx
index d8c9d4d8f..4ad4f741c 100644
--- a/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -58,9 +58,9 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
const { data: sshKeys } = api.sshKey.all.useQuery();
const router = useRouter();
- const { mutateAsync, isLoading } = api.compose.update.useMutation();
+ const { mutateAsync, isPending } = api.compose.update.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
branch: "",
repositoryURL: "",
@@ -318,7 +318,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
-
+
Save{" "}
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx
index fce562285..39f025438 100644
--- a/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -72,10 +72,10 @@ interface Props {
export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
const { data, refetch } = api.compose.one.useQuery({ composeId });
- const { mutateAsync, isLoading: isSavingGiteaProvider } =
+ const { mutateAsync, isPending: isSavingGiteaProvider } =
api.compose.update.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
composePath: "./docker-compose.yml",
repository: {
@@ -244,13 +244,13 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {isLoadingRepositories
- ? "Loading...."
- : field.value.owner
- ? repositories?.find(
+ {!field.value.owner
+ ? "Select repository"
+ : isLoadingRepositories
+ ? "Loading...."
+ : (repositories?.find(
(repo) => repo.name === field.value.repo,
- )?.name
- : "Select repository"}
+ )?.name ?? "Select repository")}
@@ -261,11 +261,15 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
- {isLoadingRepositories && (
+ {!giteaId ? (
+
+ Select a Gitea account first
+
+ ) : isLoadingRepositories ? (
Loading Repositories....
- )}
+ ) : null}
No repositories found.
@@ -327,7 +331,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx
index 5ad950e4c..827ce1a8a 100644
--- a/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -72,10 +72,10 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
const { data: githubProviders } = api.github.githubProviders.useQuery();
const { data, refetch } = api.compose.one.useQuery({ composeId });
- const { mutateAsync, isLoading: isSavingGithubProvider } =
+ const { mutateAsync, isPending: isSavingGithubProvider } =
api.compose.update.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
composePath: "./docker-compose.yml",
repository: {
@@ -94,7 +94,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
const repository = form.watch("repository");
const githubId = form.watch("githubId");
const triggerType = form.watch("triggerType");
- const { data: repositories, isLoading: isLoadingRepositories } =
+ const { data: repositories, isPending: isLoadingRepositories } =
api.github.getGithubRepositories.useQuery(
{
githubId,
@@ -234,13 +234,13 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {isLoadingRepositories
- ? "Loading...."
- : field.value.owner
- ? repositories?.find(
+ {!field.value.owner
+ ? "Select repository"
+ : isLoadingRepositories
+ ? "Loading...."
+ : (repositories?.find(
(repo) => repo.name === field.value.repo,
- )?.name
- : "Select repository"}
+ )?.name ?? "Select repository")}
@@ -252,11 +252,15 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
- {isLoadingRepositories && (
+ {!githubId ? (
+
+ Select a GitHub account first
+
+ ) : isLoadingRepositories ? (
Loading Repositories....
- )}
+ ) : null}
No repositories found.
@@ -317,7 +321,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -334,7 +338,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
- {status === "loading" && fetchStatus === "fetching" && (
+ {status === "pending" && fetchStatus === "fetching" && (
Loading Branches....
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx
index 98c2afa11..63de87d8f 100644
--- a/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link";
import { useEffect, useMemo } from "react";
@@ -74,10 +74,10 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
const { data, refetch } = api.compose.one.useQuery({ composeId });
- const { mutateAsync, isLoading: isSavingGitlabProvider } =
+ const { mutateAsync, isPending: isSavingGitlabProvider } =
api.compose.update.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
composePath: "./docker-compose.yml",
repository: {
@@ -256,13 +256,13 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {isLoadingRepositories
- ? "Loading...."
- : field.value.owner
- ? repositories?.find(
+ {!field.value.owner
+ ? "Select repository"
+ : isLoadingRepositories
+ ? "Loading...."
+ : (repositories?.find(
(repo) => repo.name === field.value.repo,
- )?.name
- : "Select repository"}
+ )?.name ?? "Select repository")}
@@ -274,11 +274,15 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
- {isLoadingRepositories && (
+ {!gitlabId ? (
+
+ Select a GitLab account first
+
+ ) : isLoadingRepositories ? (
Loading Repositories....
- )}
+ ) : null}
No repositories found.
@@ -349,7 +353,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -366,7 +370,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
- {status === "loading" && fetchStatus === "fetching" && (
+ {status === "pending" && fetchStatus === "fetching" && (
Loading Branches....
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/show.tsx b/apps/dokploy/components/dashboard/compose/general/generic/show.tsx
index 798f72249..759fe728c 100644
--- a/apps/dokploy/components/dashboard/compose/general/generic/show.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/generic/show.tsx
@@ -27,13 +27,13 @@ interface Props {
}
export const ShowProviderFormCompose = ({ composeId }: Props) => {
- const { data: githubProviders, isLoading: isLoadingGithub } =
+ const { data: githubProviders, isPending: isLoadingGithub } =
api.github.githubProviders.useQuery();
- const { data: gitlabProviders, isLoading: isLoadingGitlab } =
+ const { data: gitlabProviders, isPending: isLoadingGitlab } =
api.gitlab.gitlabProviders.useQuery();
- const { data: bitbucketProviders, isLoading: isLoadingBitbucket } =
+ const { data: bitbucketProviders, isPending: isLoadingBitbucket } =
api.bitbucket.bitbucketProviders.useQuery();
- const { data: giteaProviders, isLoading: isLoadingGitea } =
+ const { data: giteaProviders, isPending: isLoadingGitea } =
api.gitea.giteaProviders.useQuery();
const { mutateAsync: disconnectGitProvider } =
diff --git a/apps/dokploy/components/dashboard/compose/general/randomize-compose.tsx b/apps/dokploy/components/dashboard/compose/general/randomize-compose.tsx
index 2c488aefe..99c749c26 100644
--- a/apps/dokploy/components/dashboard/compose/general/randomize-compose.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/randomize-compose.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { AlertTriangle } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
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 fac6c2a34..211f5f5c7 100644
--- a/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx
@@ -32,7 +32,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
},
);
- const { mutateAsync, isLoading } = api.compose.fetchSourceType.useMutation();
+ const { mutateAsync, isPending } = api.compose.fetchSourceType.useMutation();
useEffect(() => {
if (isOpen) {
@@ -66,7 +66,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
Preview your docker-compose file with added domains. Note: At least
one domain must be specified for this conversion to take effect.
- {isLoading ? (
+ {isPending ? (
@@ -82,7 +82,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
{
mutateAsync({ composeId })
.then(() => {
diff --git a/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx
index 16dd1f246..159ab3485 100644
--- a/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx
+++ b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx
@@ -41,7 +41,7 @@ export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
const [option, setOption] = useState<"swarm" | "native">("native");
const [containerId, setContainerId] = useState();
- const { data: services, isLoading: servicesLoading } =
+ const { data: services, isPending: servicesLoading } =
api.docker.getStackContainersByAppName.useQuery(
{
appName,
@@ -52,7 +52,7 @@ export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
},
);
- const { data: containers, isLoading: containersLoading } =
+ const { data: containers, isPending: containersLoading } =
api.docker.getContainersByAppNameMatch.useQuery(
{
appName,
diff --git a/apps/dokploy/components/dashboard/compose/logs/show.tsx b/apps/dokploy/components/dashboard/compose/logs/show.tsx
index fbcdf7292..bc47f1b6e 100644
--- a/apps/dokploy/components/dashboard/compose/logs/show.tsx
+++ b/apps/dokploy/components/dashboard/compose/logs/show.tsx
@@ -1,8 +1,8 @@
import { Loader2 } from "lucide-react";
-import { badgeStateColor } from "@/components/dashboard/application/logs/show";
-import { Badge } from "@/components/ui/badge";
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 {
Card,
CardContent,
@@ -42,7 +42,7 @@ export const ShowDockerLogsCompose = ({
appType,
serverId,
}: Props) => {
- const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
+ const { data, isPending } = api.docker.getContainersByAppNameMatch.useQuery(
{
appName,
appType,
@@ -73,7 +73,7 @@ export const ShowDockerLogsCompose = ({
Select a container to view logs
- {isLoading ? (
+ {isPending ? (
Loading...
diff --git a/apps/dokploy/components/dashboard/compose/update-compose.tsx b/apps/dokploy/components/dashboard/compose/update-compose.tsx
index 7564988e2..91e04950a 100644
--- a/apps/dokploy/components/dashboard/compose/update-compose.tsx
+++ b/apps/dokploy/components/dashboard/compose/update-compose.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -43,7 +43,7 @@ interface Props {
export const UpdateCompose = ({ composeId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
- const { mutateAsync, error, isError, isLoading } =
+ const { mutateAsync, error, isError, isPending } =
api.compose.update.useMutation();
const { data } = api.compose.one.useQuery(
{
@@ -148,7 +148,7 @@ export const UpdateCompose = ({ composeId }: Props) => {
/>
diff --git a/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx b/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx
index f2ca41b85..3ef31c26f 100644
--- a/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx
+++ b/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import {
CheckIcon,
ChevronsUpDown,
@@ -192,7 +192,7 @@ export const HandleBackup = ({
}: Props) => {
const [isOpen, setIsOpen] = useState(false);
- const { data, isLoading } = api.destination.all.useQuery();
+ const { data, isPending } = api.destination.all.useQuery();
const { data: backup } = api.backup.one.useQuery(
{
backupId: backupId ?? "",
@@ -202,12 +202,12 @@ export const HandleBackup = ({
},
);
const [cacheType, setCacheType] = useState("cache");
- const { mutateAsync: createBackup, isLoading: isCreatingPostgresBackup } =
+ const { mutateAsync: createBackup, isPending: isCreatingPostgresBackup } =
backupId
? api.backup.update.useMutation()
: api.backup.create.useMutation();
- const form = useForm>({
+ const form = useForm({
defaultValues: {
database: databaseType === "web-server" ? "dokploy" : "",
destinationId: "",
@@ -396,7 +396,7 @@ export const HandleBackup = ({
!field.value && "text-muted-foreground",
)}
>
- {isLoading
+ {isPending
? "Loading...."
: field.value
? data?.find(
@@ -415,7 +415,7 @@ export const HandleBackup = ({
placeholder="Search Destination..."
className="h-9"
/>
- {isLoading && (
+ {isPending && (
Loading Destinations....
@@ -613,6 +613,7 @@ export const HandleBackup = ({
type="number"
placeholder={"keeps all the backups if left empty"}
{...field}
+ value={field.value as string}
/>
diff --git a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx
index 01f6944e1..ba8e4caf5 100644
--- a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx
+++ b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx
@@ -1,6 +1,6 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import copy from "copy-to-clipboard";
-import _ from "lodash";
+import debounce from "lodash/debounce";
import {
CheckIcon,
ChevronsUpDown,
@@ -78,27 +78,15 @@ interface Props {
const RestoreBackupSchema = z
.object({
- destinationId: z
- .string({
- required_error: "Please select a destination",
- })
- .min(1, {
- message: "Destination is required",
- }),
- backupFile: z
- .string({
- required_error: "Please select a backup file",
- })
- .min(1, {
- message: "Backup file is required",
- }),
- databaseName: z
- .string({
- required_error: "Please enter a database name",
- })
- .min(1, {
- message: "Database name is required",
- }),
+ destinationId: z.string().min(1, {
+ message: "Destination is required",
+ }),
+ backupFile: z.string().min(1, {
+ message: "Backup file is required",
+ }),
+ databaseName: z.string().min(1, {
+ message: "Database name is required",
+ }),
databaseType: z
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"])
.optional(),
@@ -219,7 +207,7 @@ export const RestoreBackup = ({
const { data: destinations = [] } = api.destination.all.useQuery();
- const form = useForm>({
+ const form = useForm({
defaultValues: {
destinationId: "",
backupFile: "",
@@ -236,7 +224,7 @@ export const RestoreBackup = ({
const currentDatabaseType = form.watch("databaseType");
const metadata = form.watch("metadata");
- const debouncedSetSearch = _.debounce((value: string) => {
+ const debouncedSetSearch = debounce((value: string) => {
setDebouncedSearchTerm(value);
}, 350);
@@ -245,7 +233,7 @@ export const RestoreBackup = ({
debouncedSetSearch(value);
};
- const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
+ const { data: files = [], isPending } = api.backup.listBackupFiles.useQuery(
{
destinationId: destionationId,
search: debouncedSearchTerm,
@@ -454,7 +442,7 @@ export const RestoreBackup = ({
onValueChange={handleSearchChange}
className="h-9"
/>
- {isLoading ? (
+ {isPending ? (
Loading backup files...
diff --git a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx
index 55a09b25f..9aa118548 100644
--- a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx
+++ b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx
@@ -89,11 +89,11 @@ export const ShowBackups = ({
const mutation = mutationMap[key as keyof typeof mutationMap];
- const { mutateAsync: manualBackup, isLoading: isManualBackup } = mutation
+ const { mutateAsync: manualBackup, isPending: isManualBackup } = mutation
? mutation
: api.backup.manualBackupMongo.useMutation();
- const { mutateAsync: deleteBackup, isLoading: isRemoving } =
+ const { mutateAsync: deleteBackup, isPending: isRemoving } =
api.backup.remove.useMutation();
return (
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 bf0173956..59b939008 100644
--- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx
+++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx
@@ -402,7 +402,7 @@ export const DockerLogsId: React.FC = ({
{filteredLogs.length > 0 ? (
filteredLogs.map((filteredLog: LogLine, index: number) => (
{
- const { data, isLoading } = api.docker.getContainers.useQuery({
+ const { data, isPending } = api.docker.getContainers.useQuery({
serverId,
});
@@ -137,7 +137,7 @@ export const ShowContainers = ({ serverId }: Props) => {
- {isLoading ? (
+ {isPending ? (
Loading...
@@ -192,7 +192,7 @@ export const ShowContainers = ({ serverId }: Props) => {
colSpan={columns.length}
className="h-24 text-center"
>
- {isLoading ? (
+ {isPending ? (
Loading...
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 7e740a1b7..288208fb1 100644
--- a/apps/dokploy/components/dashboard/file-system/show-traefik-file.tsx
+++ b/apps/dokploy/components/dashboard/file-system/show-traefik-file.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -51,7 +51,7 @@ export const ShowTraefikFile = ({ path, serverId }: Props) => {
const [canEdit, setCanEdit] = useState(true);
const [skipYamlValidation, setSkipYamlValidation] = useState(false);
- const { mutateAsync, isLoading, error, isError } =
+ const { mutateAsync, isPending, error, isError } =
api.settings.updateTraefikFile.useMutation();
const form = useForm({
@@ -182,8 +182,8 @@ routers:
Update
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 86fe71ed4..9917bc21b 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,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -48,10 +48,10 @@ interface Props {
export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
- const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
+ const { mutateAsync, isPending } = api.mariadb.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
- const form = useForm({
+ const form = useForm({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
});
@@ -140,7 +140,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
@@ -161,7 +161,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
)}
-
+
Save
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 8e996846f..9d953279c 100644
--- a/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx
+++ b/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx
@@ -28,13 +28,13 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
{ enabled: !!mariadbId },
);
- const { mutateAsync: reload, isLoading: isReloading } =
+ const { mutateAsync: reload, isPending: isReloading } =
api.mariadb.reload.useMutation();
- const { mutateAsync: start, isLoading: isStarting } =
+ const { mutateAsync: start, isPending: isStarting } =
api.mariadb.start.useMutation();
- const { mutateAsync: stop, isLoading: isStopping } =
+ const { mutateAsync: stop, isPending: isStopping } =
api.mariadb.stop.useMutation();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
diff --git a/apps/dokploy/components/dashboard/mariadb/update-mariadb.tsx b/apps/dokploy/components/dashboard/mariadb/update-mariadb.tsx
index 62486e015..d181103b3 100644
--- a/apps/dokploy/components/dashboard/mariadb/update-mariadb.tsx
+++ b/apps/dokploy/components/dashboard/mariadb/update-mariadb.tsx
@@ -1,6 +1,6 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
@@ -41,8 +41,9 @@ interface Props {
}
export const UpdateMariadb = ({ mariadbId }: Props) => {
+ const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
- const { mutateAsync, error, isError, isLoading } =
+ const { mutateAsync, error, isError, isPending } =
api.mariadb.update.useMutation();
const { data } = api.mariadb.one.useQuery(
{
@@ -79,6 +80,7 @@ export const UpdateMariadb = ({ mariadbId }: Props) => {
utils.mariadb.one.invalidate({
mariadbId: mariadbId,
});
+ setIsOpen(false);
})
.catch(() => {
toast.error("Error updating the Mariadb");
@@ -87,7 +89,7 @@ export const UpdateMariadb = ({ mariadbId }: Props) => {
};
return (
-
+
{
/>
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 acc74066f..ac79410b4 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,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -48,10 +48,10 @@ interface Props {
export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.mongo.one.useQuery({ mongoId });
- const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
+ const { mutateAsync, isPending } = api.mongo.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
- const form = useForm({
+ const form = useForm({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
});
@@ -140,7 +140,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
@@ -160,7 +160,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
)}
-
+
Save
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 23fbe51d3..47a29e6c1 100644
--- a/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx
+++ b/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx
@@ -28,13 +28,13 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
{ enabled: !!mongoId },
);
- const { mutateAsync: reload, isLoading: isReloading } =
+ const { mutateAsync: reload, isPending: isReloading } =
api.mongo.reload.useMutation();
- const { mutateAsync: start, isLoading: isStarting } =
+ const { mutateAsync: start, isPending: isStarting } =
api.mongo.start.useMutation();
- const { mutateAsync: stop, isLoading: isStopping } =
+ const { mutateAsync: stop, isPending: isStopping } =
api.mongo.stop.useMutation();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
diff --git a/apps/dokploy/components/dashboard/mongo/update-mongo.tsx b/apps/dokploy/components/dashboard/mongo/update-mongo.tsx
index e78abddbd..55bccce67 100644
--- a/apps/dokploy/components/dashboard/mongo/update-mongo.tsx
+++ b/apps/dokploy/components/dashboard/mongo/update-mongo.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -43,7 +43,7 @@ interface Props {
export const UpdateMongo = ({ mongoId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
- const { mutateAsync, error, isError, isLoading } =
+ const { mutateAsync, error, isError, isPending } =
api.mongo.update.useMutation();
const { data } = api.mongo.one.useQuery(
{
@@ -148,7 +148,7 @@ export const UpdateMongo = ({ mongoId }: Props) => {
/>
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 246ae296d..fc57221bd 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
@@ -34,7 +34,7 @@ export const ComposeFreeMonitoring = ({
appType = "stack",
serverId,
}: Props) => {
- const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
+ const { data, isPending } = api.docker.getContainersByAppNameMatch.useQuery(
{
appName: appName,
appType,
@@ -51,7 +51,7 @@ export const ComposeFreeMonitoring = ({
const [containerId, setContainerId] = useState();
- const { mutateAsync: restart, isLoading: isRestarting } =
+ const { mutateAsync: restart, isPending: isRestarting } =
api.docker.restartContainer.useMutation();
useEffect(() => {
@@ -81,7 +81,7 @@ export const ComposeFreeMonitoring = ({
value={containerAppName}
>
- {isLoading ? (
+ {isPending ? (
Loading...
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 1dd41e722..42bb361bb 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
@@ -183,12 +183,13 @@ export const ContainerFreeMonitoring = ({
setCurrentData(data);
+ const MAX_DATA_POINTS = 300;
setAcummulativeData((prevData) => ({
- cpu: [...prevData.cpu, data.cpu],
- memory: [...prevData.memory, data.memory],
- block: [...prevData.block, data.block],
- network: [...prevData.network, data.network],
- disk: [...prevData.disk, data.disk],
+ cpu: [...prevData.cpu, data.cpu].slice(-MAX_DATA_POINTS),
+ memory: [...prevData.memory, data.memory].slice(-MAX_DATA_POINTS),
+ block: [...prevData.block, data.block].slice(-MAX_DATA_POINTS),
+ network: [...prevData.network, data.network].slice(-MAX_DATA_POINTS),
+ disk: [...prevData.disk, data.disk].slice(-MAX_DATA_POINTS),
}));
};
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 026043806..1f584beea 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
@@ -39,7 +39,7 @@ export const ComposePaidMonitoring = ({
baseUrl,
token,
}: Props) => {
- const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
+ const { data, isPending } = api.docker.getContainersByAppNameMatch.useQuery(
{
appName: appName,
appType,
@@ -56,7 +56,7 @@ export const ComposePaidMonitoring = ({
const [containerId, setContainerId] = useState
();
- const { mutateAsync: restart, isLoading: isRestarting } =
+ const { mutateAsync: restart, isPending: isRestarting } =
api.docker.restartContainer.useMutation();
useEffect(() => {
@@ -87,7 +87,7 @@ export const ComposePaidMonitoring = ({
value={containerAppName}
>
- {isLoading ? (
+ {isPending ? (
Loading...
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 6e6cbe018..b9ddad916 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,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -48,10 +48,10 @@ interface Props {
export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
- const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
+ const { mutateAsync, isPending } = api.mysql.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
- const form = useForm
({
+ const form = useForm({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
});
@@ -140,7 +140,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
@@ -160,7 +160,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
)}
-
+
Save
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 045a717b7..1a55c1d1a 100644
--- a/apps/dokploy/components/dashboard/mysql/general/show-general-mysql.tsx
+++ b/apps/dokploy/components/dashboard/mysql/general/show-general-mysql.tsx
@@ -28,12 +28,12 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
{ enabled: !!mysqlId },
);
- const { mutateAsync: reload, isLoading: isReloading } =
+ const { mutateAsync: reload, isPending: isReloading } =
api.mysql.reload.useMutation();
- const { mutateAsync: start, isLoading: isStarting } =
+ const { mutateAsync: start, isPending: isStarting } =
api.mysql.start.useMutation();
- const { mutateAsync: stop, isLoading: isStopping } =
+ const { mutateAsync: stop, isPending: isStopping } =
api.mysql.stop.useMutation();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
diff --git a/apps/dokploy/components/dashboard/mysql/update-mysql.tsx b/apps/dokploy/components/dashboard/mysql/update-mysql.tsx
index 353523aa0..3442d44e3 100644
--- a/apps/dokploy/components/dashboard/mysql/update-mysql.tsx
+++ b/apps/dokploy/components/dashboard/mysql/update-mysql.tsx
@@ -1,6 +1,6 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
@@ -41,8 +41,9 @@ interface Props {
}
export const UpdateMysql = ({ mysqlId }: Props) => {
+ const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
- const { mutateAsync, error, isError, isLoading } =
+ const { mutateAsync, error, isError, isPending } =
api.mysql.update.useMutation();
const { data } = api.mysql.one.useQuery(
{
@@ -79,6 +80,7 @@ export const UpdateMysql = ({ mysqlId }: Props) => {
utils.mysql.one.invalidate({
mysqlId: mysqlId,
});
+ setIsOpen(false);
})
.catch(() => {
toast.error("Error updating MySQL");
@@ -87,7 +89,7 @@ export const UpdateMysql = ({ mysqlId }: Props) => {
};
return (
-
+
{
/>
diff --git a/apps/dokploy/components/dashboard/organization/handle-organization.tsx b/apps/dokploy/components/dashboard/organization/handle-organization.tsx
index c676e0233..c191bead5 100644
--- a/apps/dokploy/components/dashboard/organization/handle-organization.tsx
+++ b/apps/dokploy/components/dashboard/organization/handle-organization.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon, Plus } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -52,7 +52,7 @@ export function AddOrganization({ organizationId }: Props) {
enabled: !!organizationId,
},
);
- const { mutateAsync, isLoading } = organizationId
+ const { mutateAsync, isPending } = organizationId
? api.organization.update.useMutation()
: api.organization.create.useMutation();
const { refetch: refetchActiveOrganization } =
@@ -177,7 +177,7 @@ export function AddOrganization({ organizationId }: Props) {
)}
/>
-
+
{organizationId ? "Update organization" : "Create organization"}
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 d9841716e..0921984ac 100644
--- a/apps/dokploy/components/dashboard/postgres/advanced/show-custom-command.tsx
+++ b/apps/dokploy/components/dashboard/postgres/advanced/show-custom-command.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Plus, Trash2 } from "lucide-react";
import { useEffect } from "react";
import { useFieldArray, useForm } from "react-hook-form";
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 1d34c010a..c38240a3f 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,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -48,12 +48,12 @@ interface Props {
export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.postgres.one.useQuery({ postgresId });
- const { mutateAsync, isLoading } =
+ const { mutateAsync, isPending } =
api.postgres.saveExternalPort.useMutation();
const getIp = data?.server?.ipAddress || ip;
const [connectionUrl, setConnectionUrl] = useState("");
- const form = useForm({
+ const form = useForm({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
});
@@ -142,7 +142,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
@@ -162,7 +162,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
)}
-
+
Save
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 de520053d..0e6b87e9e 100644
--- a/apps/dokploy/components/dashboard/postgres/general/show-general-postgres.tsx
+++ b/apps/dokploy/components/dashboard/postgres/general/show-general-postgres.tsx
@@ -28,13 +28,13 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
{ enabled: !!postgresId },
);
- const { mutateAsync: reload, isLoading: isReloading } =
+ const { mutateAsync: reload, isPending: isReloading } =
api.postgres.reload.useMutation();
- const { mutateAsync: stop, isLoading: isStopping } =
+ const { mutateAsync: stop, isPending: isStopping } =
api.postgres.stop.useMutation();
- const { mutateAsync: start, isLoading: isStarting } =
+ const { mutateAsync: start, isPending: isStarting } =
api.postgres.start.useMutation();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
diff --git a/apps/dokploy/components/dashboard/postgres/update-postgres.tsx b/apps/dokploy/components/dashboard/postgres/update-postgres.tsx
index d4485862e..c83604b54 100644
--- a/apps/dokploy/components/dashboard/postgres/update-postgres.tsx
+++ b/apps/dokploy/components/dashboard/postgres/update-postgres.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBox } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -43,7 +43,7 @@ interface Props {
export const UpdatePostgres = ({ postgresId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
- const { mutateAsync, error, isError, isLoading } =
+ const { mutateAsync, error, isError, isPending } =
api.postgres.update.useMutation();
const { data } = api.postgres.one.useQuery(
{
@@ -148,7 +148,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
/>
{
// 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 } =
+ const { mutateAsync, isPending, error, isError } =
api.application.create.useMutation();
const form = useForm({
@@ -283,7 +283,7 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
-
+
Create
diff --git a/apps/dokploy/components/dashboard/project/add-compose.tsx b/apps/dokploy/components/dashboard/project/add-compose.tsx
index bb911373f..815c58ca8 100644
--- a/apps/dokploy/components/dashboard/project/add-compose.tsx
+++ b/apps/dokploy/components/dashboard/project/add-compose.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CircuitBoard, HelpCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -75,7 +75,7 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
const slug = slugify(projectName);
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: servers } = api.server.withSSHKey.useQuery();
- const { mutateAsync, isLoading, error, isError } =
+ const { mutateAsync, isPending, error, isError } =
api.compose.create.useMutation();
// Get environment data to extract projectId
@@ -307,7 +307,7 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
-
+
Create
diff --git a/apps/dokploy/components/dashboard/project/add-database.tsx b/apps/dokploy/components/dashboard/project/add-database.tsx
index 67d00b0d7..e14653880 100644
--- a/apps/dokploy/components/dashboard/project/add-database.tsx
+++ b/apps/dokploy/components/dashboard/project/add-database.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { AlertTriangle, Database, HelpCircle } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -52,7 +52,7 @@ import {
import { slugify } from "@/lib/slug";
import { api } from "@/utils/api";
-type DbType = typeof mySchema._type.type;
+type DbType = z.infer["type"];
const dockerImageDefaultPlaceholder: Record = {
mongo: "mongo:7",
@@ -196,7 +196,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
const shouldShowServerDropdown = hasServers;
- const form = useForm({
+ const form = useForm({
defaultValues: {
type: "postgres",
dockerImage: "",
diff --git a/apps/dokploy/components/dashboard/project/add-template.tsx b/apps/dokploy/components/dashboard/project/add-template.tsx
index 72c42da49..ef9a88e6f 100644
--- a/apps/dokploy/components/dashboard/project/add-template.tsx
+++ b/apps/dokploy/components/dashboard/project/add-template.tsx
@@ -116,7 +116,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
);
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: servers } = api.server.withSSHKey.useQuery();
- const { data: tags, isLoading: isLoadingTags } = api.compose.getTags.useQuery(
+ const { data: tags, isPending: isLoadingTags } = api.compose.getTags.useQuery(
{ baseUrl: customBaseUrl },
{
enabled: open,
@@ -125,7 +125,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
const utils = api.useUtils();
const [serverId, setServerId] = useState(undefined);
- const { mutateAsync, isLoading, error, isError } =
+ const { mutateAsync, isPending, error, isError } =
api.compose.deployTemplate.useMutation();
const templates =
@@ -512,7 +512,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
Cancel
{
const promise = mutateAsync({
serverId:
diff --git a/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx b/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx
index 678928990..3e28a248b 100644
--- a/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx
+++ b/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx
@@ -93,7 +93,7 @@ export const AdvancedEnvironmentSelector = ({
await createEnvironment.mutateAsync({
projectId,
name: name.trim(),
- description: description.trim() || null,
+ description: description.trim() || undefined,
});
toast.success("Environment created successfully");
@@ -115,7 +115,7 @@ export const AdvancedEnvironmentSelector = ({
await updateEnvironment.mutateAsync({
environmentId: selectedEnvironment.environmentId,
name: name.trim(),
- description: description.trim() || null,
+ description: description.trim() || undefined,
});
toast.success("Environment updated successfully");
@@ -168,7 +168,7 @@ export const AdvancedEnvironmentSelector = ({
const result = await duplicateEnvironment.mutateAsync({
environmentId: environment.environmentId,
name: `${environment.name}-copy`,
- description: environment.description,
+ description: environment.description || undefined,
});
toast.success("Environment duplicated successfully");
@@ -334,9 +334,9 @@ export const AdvancedEnvironmentSelector = ({
- {createEnvironment.isLoading ? "Creating..." : "Create"}
+ {createEnvironment.isPending ? "Creating..." : "Create"}
@@ -387,9 +387,9 @@ export const AdvancedEnvironmentSelector = ({
- {updateEnvironment.isLoading ? "Updating..." : "Update"}
+ {updateEnvironment.isPending ? "Updating..." : "Update"}
@@ -427,12 +427,12 @@ export const AdvancedEnvironmentSelector = ({
variant="destructive"
onClick={handleDeleteEnvironment}
disabled={
- deleteEnvironment.isLoading ||
+ deleteEnvironment.isPending ||
haveServices ||
!selectedEnvironment
}
>
- {deleteEnvironment.isLoading ? "Deleting..." : "Delete"}
+ {deleteEnvironment.isPending ? "Deleting..." : "Delete"}
diff --git a/apps/dokploy/components/dashboard/project/ai/step-two.tsx b/apps/dokploy/components/dashboard/project/ai/step-two.tsx
index 09484bc57..e13ff40ad 100644
--- a/apps/dokploy/components/dashboard/project/ai/step-two.tsx
+++ b/apps/dokploy/components/dashboard/project/ai/step-two.tsx
@@ -28,7 +28,7 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
const suggestions = templateInfo.suggestions || [];
const selectedVariant = templateInfo.details;
- const { mutateAsync, isLoading, error, isError } =
+ const { mutateAsync, isPending, error, isError } =
api.ai.suggest.useMutation();
useEffect(() => {
@@ -184,7 +184,7 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
);
}
- if (isLoading) {
+ if (isPending) {
return (
diff --git a/apps/dokploy/components/dashboard/project/duplicate-project.tsx b/apps/dokploy/components/dashboard/project/duplicate-project.tsx
index 3455f34cf..e754b1d8b 100644
--- a/apps/dokploy/components/dashboard/project/duplicate-project.tsx
+++ b/apps/dokploy/components/dashboard/project/duplicate-project.tsx
@@ -25,7 +25,6 @@ import {
import { api } from "@/utils/api";
export type Services = {
- appName: string;
serverId?: string | null;
name: string;
type:
@@ -76,7 +75,7 @@ export const DuplicateProject = ({
selectedServiceIds.includes(service.id),
);
- const { mutateAsync: duplicateProject, isLoading } =
+ const { mutateAsync: duplicateProject, isPending } =
api.project.duplicate.useMutation({
onSuccess: async (newProject) => {
await utils.project.all.invalidate();
@@ -321,20 +320,20 @@ export const DuplicateProject = ({
setOpen(false)}
- disabled={isLoading}
+ disabled={isPending}
>
Cancel
- {isLoading ? (
+ {isPending ? (
<>
{duplicateType === "new-project"
diff --git a/apps/dokploy/components/dashboard/project/environment-variables.tsx b/apps/dokploy/components/dashboard/project/environment-variables.tsx
index e833fa779..13ca99448 100644
--- a/apps/dokploy/components/dashboard/project/environment-variables.tsx
+++ b/apps/dokploy/components/dashboard/project/environment-variables.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Terminal } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -41,7 +41,7 @@ interface Props {
export const EnvironmentVariables = ({ environmentId, children }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
- const { mutateAsync, error, isError, isLoading } =
+ const { mutateAsync, error, isError, isPending } =
api.environment.update.useMutation();
const { data } = api.environment.one.useQuery(
{
@@ -85,7 +85,7 @@ export const EnvironmentVariables = ({ environmentId, children }: Props) => {
// Add keyboard shortcut for Ctrl+S/Cmd+S
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
- if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading && isOpen) {
+ if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isPending && isOpen) {
e.preventDefault();
form.handleSubmit(onSubmit)();
}
@@ -95,7 +95,7 @@ export const EnvironmentVariables = ({ environmentId, children }: Props) => {
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
- }, [form, onSubmit, isLoading, isOpen]);
+ }, [form, onSubmit, isPending, isOpen]);
return (
@@ -158,7 +158,7 @@ API_KEY=your-api-key-here
)}
/>
-
+
Update
diff --git a/apps/dokploy/components/dashboard/projects/handle-project.tsx b/apps/dokploy/components/dashboard/projects/handle-project.tsx
index 09fd36f84..d3305e864 100644
--- a/apps/dokploy/components/dashboard/projects/handle-project.tsx
+++ b/apps/dokploy/components/dashboard/projects/handle-project.tsx
@@ -1,4 +1,5 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
+
import { PlusIcon, SquarePen } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
@@ -80,7 +81,7 @@ export const HandleProject = ({ projectId }: Props) => {
description: "",
name: "",
},
- resolver: zodResolver(AddProjectSchema),
+ resolver: standardSchemaResolver(AddProjectSchema),
});
useEffect(() => {
diff --git a/apps/dokploy/components/dashboard/projects/project-environment.tsx b/apps/dokploy/components/dashboard/projects/project-environment.tsx
index cb6245f08..b02f9024a 100644
--- a/apps/dokploy/components/dashboard/projects/project-environment.tsx
+++ b/apps/dokploy/components/dashboard/projects/project-environment.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { FileIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -41,7 +41,7 @@ interface Props {
export const ProjectEnvironment = ({ projectId, children }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
- const { mutateAsync, error, isError, isLoading } =
+ const { mutateAsync, error, isError, isPending } =
api.project.update.useMutation();
const { data } = api.project.one.useQuery(
{
@@ -84,7 +84,7 @@ export const ProjectEnvironment = ({ projectId, children }: Props) => {
// Add keyboard shortcut for Ctrl+S/Cmd+S
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
- if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading && isOpen) {
+ if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isPending && isOpen) {
e.preventDefault();
form.handleSubmit(onSubmit)();
}
@@ -94,7 +94,7 @@ export const ProjectEnvironment = ({ projectId, children }: Props) => {
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
- }, [form, onSubmit, isLoading, isOpen]);
+ }, [form, onSubmit, isPending, isOpen]);
return (
@@ -155,7 +155,7 @@ PORT=3000
)}
/>
-
+
Update
diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx
index 8234593e1..f25fb6d47 100644
--- a/apps/dokploy/components/dashboard/projects/show.tsx
+++ b/apps/dokploy/components/dashboard/projects/show.tsx
@@ -2,7 +2,6 @@ import {
AlertTriangle,
ArrowUpDown,
BookIcon,
- ExternalLinkIcon,
FolderInput,
Loader2,
MoreHorizontalIcon,
@@ -16,7 +15,6 @@ import { toast } from "sonner";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DateTooltip } from "@/components/shared/date-tooltip";
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
-import { StatusTooltip } from "@/components/shared/status-tooltip";
import {
AlertDialog,
AlertDialogAction,
@@ -40,10 +38,8 @@ import {
import {
DropdownMenu,
DropdownMenuContent,
- DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
- DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
@@ -63,7 +59,7 @@ export const ShowProjects = () => {
const utils = api.useUtils();
const router = useRouter();
const { data: isCloud } = api.settings.isCloud.useQuery();
- const { data, isLoading } = api.project.all.useQuery();
+ const { data, isPending } = api.project.all.useQuery();
const { data: auth } = api.user.get.useQuery();
const { mutateAsync } = api.project.remove.useMutation();
@@ -200,7 +196,7 @@ export const ShowProjects = () => {
- {isLoading ? (
+ {isPending ? (
Loading...
@@ -280,14 +276,6 @@ export const ShowProjects = () => {
)
.reduce((acc, curr) => acc + curr, 0);
- const haveServicesWithDomains = project?.environments
- .map(
- (env) =>
- env.applications.length > 0 ||
- env.compose.length > 0,
- )
- .some(Boolean);
-
// Find default environment from accessible environments, or fall back to first accessible environment
const accessibleEnvironment =
project?.environments.find((env) => env.isDefault) ||
@@ -313,124 +301,8 @@ export const ShowProjects = () => {
}}
>
- {haveServicesWithDomains ? (
-
-
-
-
-
-
- e.stopPropagation()}
- >
- {project.environments.some(
- (env) => env.applications.length > 0,
- ) && (
-
-
- Applications
-
- {project.environments.map((env) =>
- env.applications.map((app) => (
-
-
-
-
- {app.name}
-
-
-
- {app.domains.map((domain) => (
-
-
-
- {domain.host}
-
-
-
-
- ))}
-
-
- )),
- )}
-
- )}
- {project.environments.some(
- (env) => env.compose.length > 0,
- ) && (
-
-
- Compose
-
- {project.environments.map((env) =>
- env.compose.map((comp) => (
-
-
-
-
- {comp.name}
-
-
-
- {comp.domains.map((domain) => (
-
-
-
- {domain.host}
-
-
-
-
- ))}
-
-
- )),
- )}
-
- )}
-
-
- ) : null}
-
+
@@ -439,7 +311,7 @@ export const ShowProjects = () => {
-
+
{project.description}
diff --git a/apps/dokploy/components/dashboard/redis/general/show-external-redis-credentials.tsx b/apps/dokploy/components/dashboard/redis/general/show-external-redis-credentials.tsx
index 9511af628..ebc01200a 100644
--- a/apps/dokploy/components/dashboard/redis/general/show-external-redis-credentials.tsx
+++ b/apps/dokploy/components/dashboard/redis/general/show-external-redis-credentials.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -48,11 +48,11 @@ interface Props {
export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.redis.one.useQuery({ redisId });
- const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
+ const { mutateAsync, isPending } = api.redis.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
- const form = useForm({
+ const form = useForm({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
});
@@ -134,7 +134,7 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
@@ -154,7 +154,7 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
)}
-
+
Save
diff --git a/apps/dokploy/components/dashboard/redis/general/show-general-redis.tsx b/apps/dokploy/components/dashboard/redis/general/show-general-redis.tsx
index de70cc558..4300f9af3 100644
--- a/apps/dokploy/components/dashboard/redis/general/show-general-redis.tsx
+++ b/apps/dokploy/components/dashboard/redis/general/show-general-redis.tsx
@@ -28,12 +28,12 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
{ enabled: !!redisId },
);
- const { mutateAsync: reload, isLoading: isReloading } =
+ const { mutateAsync: reload, isPending: isReloading } =
api.redis.reload.useMutation();
- const { mutateAsync: start, isLoading: isStarting } =
+ const { mutateAsync: start, isPending: isStarting } =
api.redis.start.useMutation();
- const { mutateAsync: stop, isLoading: isStopping } =
+ const { mutateAsync: stop, isPending: isStopping } =
api.redis.stop.useMutation();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
diff --git a/apps/dokploy/components/dashboard/redis/update-redis.tsx b/apps/dokploy/components/dashboard/redis/update-redis.tsx
index 7d17552fa..b20e6b0c4 100644
--- a/apps/dokploy/components/dashboard/redis/update-redis.tsx
+++ b/apps/dokploy/components/dashboard/redis/update-redis.tsx
@@ -1,6 +1,6 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
@@ -41,8 +41,9 @@ interface Props {
}
export const UpdateRedis = ({ redisId }: Props) => {
+ const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
- const { mutateAsync, error, isError, isLoading } =
+ const { mutateAsync, error, isError, isPending } =
api.redis.update.useMutation();
const { data } = api.redis.one.useQuery(
{
@@ -79,6 +80,7 @@ export const UpdateRedis = ({ redisId }: Props) => {
utils.redis.one.invalidate({
redisId: redisId,
});
+ setIsOpen(false);
})
.catch(() => {
toast.error("Error updating Redis");
@@ -87,7 +89,7 @@ export const UpdateRedis = ({ redisId }: Props) => {
};
return (
-
+
{
/>
diff --git a/apps/dokploy/components/dashboard/requests/columns.tsx b/apps/dokploy/components/dashboard/requests/columns.tsx
index 3648261fb..997074fde 100644
--- a/apps/dokploy/components/dashboard/requests/columns.tsx
+++ b/apps/dokploy/components/dashboard/requests/columns.tsx
@@ -6,6 +6,9 @@ import { Button } from "@/components/ui/button";
import type { LogEntry } from "./show-requests";
export const getStatusColor = (status: number) => {
+ if (status === 0) {
+ return "secondary";
+ }
if (status >= 100 && status < 200) {
return "outline";
}
@@ -21,6 +24,24 @@ export const getStatusColor = (status: number) => {
return "destructive";
};
+const formatStatusLabel = (status: number) => {
+ if (status === 0) {
+ return "N/A";
+ }
+ return status;
+};
+
+const formatDuration = (nanos: number) => {
+ const ms = nanos / 1000000;
+ if (ms < 1) {
+ return `${(nanos / 1000).toFixed(2)} µs`;
+ }
+ if (ms < 1000) {
+ return `${ms.toFixed(2)} ms`;
+ }
+ return `${(ms / 1000).toFixed(2)} s`;
+};
+
export const columns: ColumnDef[] = [
{
accessorKey: "level",
@@ -59,10 +80,10 @@ export const columns: ColumnDef[] = [
- Status: {log.OriginStatus}
+ Status: {formatStatusLabel(log.OriginStatus)}
- Exec Time: {`${log.Duration / 1000000000}s`}
+ Exec Time: {formatDuration(log.Duration)}
IP: {log.ClientAddr}
diff --git a/apps/dokploy/components/dashboard/requests/requests-table.tsx b/apps/dokploy/components/dashboard/requests/requests-table.tsx
index 45a531324..e804b065b 100644
--- a/apps/dokploy/components/dashboard/requests/requests-table.tsx
+++ b/apps/dokploy/components/dashboard/requests/requests-table.tsx
@@ -152,7 +152,15 @@ export const RequestsTable = ({ dateRange }: RequestsTableProps) => {
return JSON.stringify(value, null, 2);
}
if (key === "Duration" || key === "OriginDuration" || key === "Overhead") {
- return `${value / 1000000000} s`;
+ const nanos = Number(value);
+ const ms = nanos / 1000000;
+ if (ms < 1) {
+ return `${(nanos / 1000).toFixed(2)} µs`;
+ }
+ if (ms < 1000) {
+ return `${ms.toFixed(2)} ms`;
+ }
+ return `${(ms / 1000).toFixed(2)} s`;
}
if (key === "level") {
return {value} ;
@@ -161,7 +169,11 @@ export const RequestsTable = ({ dateRange }: RequestsTableProps) => {
return {value} ;
}
if (key === "DownstreamStatus" || key === "OriginStatus") {
- return {value} ;
+ const num = Number(value);
+ if (num === 0) {
+ return N/A ;
+ }
+ return {value} ;
}
return value;
};
diff --git a/apps/dokploy/components/dashboard/settings/ai-form.tsx b/apps/dokploy/components/dashboard/settings/ai-form.tsx
index f7f81c9cf..c3518fdcc 100644
--- a/apps/dokploy/components/dashboard/settings/ai-form.tsx
+++ b/apps/dokploy/components/dashboard/settings/ai-form.tsx
@@ -15,8 +15,8 @@ import { api } from "@/utils/api";
import { HandleAi } from "./handle-ai";
export const AiForm = () => {
- const { data: aiConfigs, refetch, isLoading } = api.ai.getAll.useQuery();
- const { mutateAsync, isLoading: isRemoving } = api.ai.delete.useMutation();
+ const { data: aiConfigs, refetch, isPending } = api.ai.getAll.useQuery();
+ const { mutateAsync, isPending: isRemoving } = api.ai.delete.useMutation();
return (
@@ -33,7 +33,7 @@ export const AiForm = () => {
{aiConfigs && aiConfigs?.length > 0 &&
}
- {isLoading ? (
+ {isPending ? (
Loading...
diff --git a/apps/dokploy/components/dashboard/settings/api/add-api-key.tsx b/apps/dokploy/components/dashboard/settings/api/add-api-key.tsx
index 15c7ed6e0..c6db49b5d 100644
--- a/apps/dokploy/components/dashboard/settings/api/add-api-key.tsx
+++ b/apps/dokploy/components/dashboard/settings/api/add-api-key.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import copy from "copy-to-clipboard";
import { useState } from "react";
import { useForm } from "react-hook-form";
diff --git a/apps/dokploy/components/dashboard/settings/api/show-api-keys.tsx b/apps/dokploy/components/dashboard/settings/api/show-api-keys.tsx
index efa68929f..2e90a9bb6 100644
--- a/apps/dokploy/components/dashboard/settings/api/show-api-keys.tsx
+++ b/apps/dokploy/components/dashboard/settings/api/show-api-keys.tsx
@@ -17,7 +17,7 @@ import { AddApiKey } from "./add-api-key";
export const ShowApiKeys = () => {
const { data, refetch } = api.user.get.useQuery();
- const { mutateAsync: deleteApiKey, isLoading: isLoadingDelete } =
+ const { mutateAsync: deleteApiKey, isPending: isLoadingDelete } =
api.user.deleteApiKey.useMutation();
return (
diff --git a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx
index 1460244c1..6e56bdd70 100644
--- a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx
+++ b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx
@@ -11,7 +11,9 @@ import {
} from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
-import { useState } from "react";
+import { useEffect, 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";
import {
@@ -31,6 +33,7 @@ const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!,
);
+/** Precio legacy / Hobby: $4.50/mo primer servidor, $3.50 siguientes; anual $45.90 primero, $35.70 siguientes. */
export const calculatePrice = (count: number, isAnnual = false) => {
if (isAnnual) {
if (count <= 1) return 45.9;
@@ -40,6 +43,27 @@ export const calculatePrice = (count: number, isAnnual = false) => {
return count * 3.5;
};
+/** Hobby: $4.50/mo per server; annual 20% off = $43.20/yr per server (4.5 * 12 * 0.8). */
+export const calculatePriceHobby = (count: number, isAnnual = false) => {
+ const perServerMonthly = 4.5;
+ const perServerAnnual = 43.2; // 4.5 * 12 * 0.8
+ return isAnnual ? count * perServerAnnual : count * perServerMonthly;
+};
+
+/** Startup: 3 servers included ($15/mo); extra servers $4.50/mo each. Annual 20% off. */
+export const STARTUP_SERVERS_INCLUDED = 3;
+export const calculatePriceStartup = (count: number, isAnnual = false) => {
+ const baseMonthly = 15;
+ const extraMonthly = 4.5;
+ const baseAnnual = 144; // 15 * 12 * 0.8
+ const extraAnnual = 43.2; // 4.5 * 12 * 0.8, consistent with Hobby annual
+ if (count <= STARTUP_SERVERS_INCLUDED)
+ return isAnnual ? baseAnnual : baseMonthly;
+ return isAnnual
+ ? baseAnnual + (count - STARTUP_SERVERS_INCLUDED) * extraAnnual
+ : baseMonthly + (count - STARTUP_SERVERS_INCLUDED) * extraMonthly;
+};
+
const navigationItems = [
{
name: "Subscription",
@@ -57,22 +81,41 @@ export const ShowBilling = () => {
const router = useRouter();
const { data: servers } = api.server.count.useQuery();
const { data: admin } = api.user.get.useQuery();
- const { data, isLoading } = api.stripe.getProducts.useQuery();
+ const { data, isPending } = api.stripe.getProducts.useQuery();
const { mutateAsync: createCheckoutSession } =
api.stripe.createCheckoutSession.useMutation();
const { mutateAsync: createCustomerPortalSession } =
api.stripe.createCustomerPortalSession.useMutation();
+ const { mutateAsync: upgradeSubscription, isPending: isUpgrading } =
+ api.stripe.upgradeSubscription.useMutation();
+ const utils = api.useUtils();
const [serverQuantity, setServerQuantity] = useState(3);
const [isAnnual, setIsAnnual] = useState(false);
+ const [upgradeTier, setUpgradeTier] = useState<"hobby" | "startup" | null>(
+ null,
+ );
+ const [upgradeServerQty, setUpgradeServerQty] = useState(3);
+ /** Billing interval in the upgrade/update form; synced to current when data loads. */
+ const [updateFormAnnual, setUpdateFormAnnual] = useState(false);
- const handleCheckout = async (productId: string) => {
+ useEffect(() => {
+ if (data?.isAnnualCurrent !== undefined) {
+ setUpdateFormAnnual(data.isAnnualCurrent);
+ }
+ }, [data?.isAnnualCurrent]);
+
+ const handleCheckout = async (
+ tier: "legacy" | "hobby" | "startup",
+ productId: string,
+ ) => {
const stripe = await stripePromise;
if (data && data.subscriptions.length === 0) {
createCheckoutSession({
+ tier,
productId,
- serverQuantity: serverQuantity,
+ serverQuantity,
isAnnual,
}).then(async (session) => {
await stripe?.redirectToCheckout({
@@ -81,6 +124,8 @@ export const ShowBilling = () => {
});
}
};
+
+ const useNewPricing = data?.hobbyProductId && data?.startupProductId;
const products = data?.products.filter((product) => {
// @ts-ignore
const interval = product?.default_price?.recurring?.interval;
@@ -93,7 +138,7 @@ export const ShowBilling = () => {
return (
-
+
@@ -128,17 +173,6 @@ export const ShowBilling = () => {
-
setIsAnnual(e === "annual")}
- >
-
- Monthly
- Annual
-
-
{admin?.user.stripeSubscriptionId && (
Servers Plan
@@ -160,6 +194,429 @@ export const ShowBilling = () => {
)}
)}
+ {/* Upgrade: solo para usuarios en plan legacy con nuevos planes disponibles */}
+ {useNewPricing &&
+ data?.currentPlan === "legacy" &&
+ data?.subscriptions?.length > 0 && (
+
+
Upgrade your plan
+
+ You’re on the legacy plan. Switch to Hobby or Startup
+ (same benefits). You can also choose annual billing (20%
+ off). Stripe will prorate the change.
+
+
+
+ Billing interval
+
+
+ setUpdateFormAnnual(false)}
+ >
+ Monthly
+
+ setUpdateFormAnnual(true)}
+ >
+ Annual (20% off)
+
+
+
+
New plan
+
+ setUpgradeTier("hobby")}
+ >
+ Hobby
+
+ setUpgradeTier("startup")}
+ >
+ Startup
+
+
+
+ {upgradeTier && (
+
+
+ Servers
+ {upgradeTier === "startup" &&
+ ` (min. ${STARTUP_SERVERS_INCLUDED})`}
+
+
+
+ setUpgradeServerQty((q) =>
+ Math.max(
+ upgradeTier === "startup"
+ ? STARTUP_SERVERS_INCLUDED
+ : 1,
+ q - 1,
+ ),
+ )
+ }
+ >
+
+
+
{
+ const v =
+ Number((e.target as HTMLInputElement).value) ||
+ 0;
+ setUpgradeServerQty(
+ Math.max(
+ upgradeTier === "startup"
+ ? STARTUP_SERVERS_INCLUDED
+ : 1,
+ v,
+ ),
+ );
+ }}
+ className="w-20 h-8"
+ />
+ setUpgradeServerQty((q) => q + 1)}
+ >
+
+
+
+
+ {upgradeTier === "hobby"
+ ? `$${calculatePriceHobby(upgradeServerQty, updateFormAnnual).toFixed(2)} per ${updateFormAnnual ? "year" : "month"}`
+ : `$${calculatePriceStartup(upgradeServerQty, updateFormAnnual).toFixed(2)} per ${updateFormAnnual ? "year" : "month"}`}
+
+
+
+ Current plan: Legacy
+
+
+ New plan:{" "}
+ {upgradeTier === "startup"
+ ? "Startup"
+ : "Hobby"}{" "}
+ · {upgradeServerQty} server
+ {upgradeServerQty !== 1 ? "s" : ""} · $
+ {upgradeTier === "hobby"
+ ? calculatePriceHobby(
+ upgradeServerQty,
+ updateFormAnnual,
+ ).toFixed(2)
+ : calculatePriceStartup(
+ upgradeServerQty,
+ updateFormAnnual,
+ ).toFixed(2)}
+ /{updateFormAnnual ? "yr" : "mo"} (
+ {updateFormAnnual ? "annual" : "monthly"})
+
+
+ Stripe will prorate the change.
+
+
+ }
+ type="default"
+ onClick={async () => {
+ if (!upgradeTier) return;
+ try {
+ await upgradeSubscription({
+ tier: upgradeTier,
+ serverQuantity: upgradeServerQty,
+ isAnnual: updateFormAnnual,
+ });
+ await utils.stripe.getProducts.invalidate();
+ await utils.user.get.invalidate();
+ setUpgradeTier(null);
+ toast.success("Plan upgraded successfully");
+ } catch {
+ toast.error("Error upgrading plan");
+ }
+ }}
+ >
+
+ {isUpgrading ? (
+ <>
+
+ Upgrading…
+ >
+ ) : (
+ "Upgrade plan"
+ )}
+
+
+
+ )}
+
+ )}
+ {/* Cambiar plan o cantidad de servidores (usuarios en Hobby o Startup; el portal no permite esto) */}
+ {useNewPricing &&
+ (data?.currentPlan === "hobby" ||
+ data?.currentPlan === "startup") &&
+ data?.subscriptions?.length > 0 && (
+
+
+ Change plan or number of servers
+
+
+ Your current plan:{" "}
+
+ {data?.currentPlan === "startup" ? "Startup" : "Hobby"}
+
+ {" · "}
+
+ {admin?.user.serversQuantity ?? 0} server
+ {(admin?.user.serversQuantity ?? 0) !== 1 ? "s" : ""}
+
+ {data?.currentPriceAmount != null && (
+ <>
+ {" · "}
+
+ ${data.currentPriceAmount.toFixed(2)}/
+ {data?.isAnnualCurrent ? "yr" : "mo"}
+
+ >
+ )}{" "}
+ ({data?.isAnnualCurrent ? "annual" : "monthly"} billing).
+
+
+ Add more servers, switch between Hobby and Startup, or
+ change to annual billing (20% off). Stripe will prorate
+ the change.
+
+
+
+ Billing interval
+
+
+ setUpdateFormAnnual(false)}
+ >
+ Monthly
+
+ setUpdateFormAnnual(true)}
+ >
+ Annual (20% off)
+
+
+
+
Plan
+
+ setUpgradeTier("hobby")}
+ >
+ Hobby
+
+ setUpgradeTier("startup")}
+ >
+ Startup
+
+
+
+ {upgradeTier && (
+
+
+ Servers
+ {upgradeTier === "startup" &&
+ ` (min. ${STARTUP_SERVERS_INCLUDED})`}
+
+
+
+ setUpgradeServerQty((q) =>
+ Math.max(
+ upgradeTier === "startup"
+ ? STARTUP_SERVERS_INCLUDED
+ : 1,
+ q - 1,
+ ),
+ )
+ }
+ >
+
+
+
{
+ const v =
+ Number((e.target as HTMLInputElement).value) ||
+ 0;
+ setUpgradeServerQty(
+ Math.max(
+ upgradeTier === "startup"
+ ? STARTUP_SERVERS_INCLUDED
+ : 1,
+ v,
+ ),
+ );
+ }}
+ className="w-20 h-8"
+ />
+ setUpgradeServerQty((q) => q + 1)}
+ >
+
+
+
+
+ {upgradeTier === "hobby"
+ ? `$${calculatePriceHobby(upgradeServerQty, updateFormAnnual).toFixed(2)} per ${updateFormAnnual ? "year" : "month"}`
+ : `$${calculatePriceStartup(upgradeServerQty, updateFormAnnual).toFixed(2)} per ${updateFormAnnual ? "year" : "month"}`}
+
+
+
+ Current plan:{" "}
+ {data?.currentPlan === "startup"
+ ? "Startup"
+ : "Hobby"}{" "}
+ · {admin?.user.serversQuantity ?? 0} server
+ {(admin?.user.serversQuantity ?? 0) !== 1
+ ? "s"
+ : ""}{" "}
+ ·{" "}
+ {data?.currentPriceAmount != null
+ ? `$${data.currentPriceAmount.toFixed(2)}/${data?.isAnnualCurrent ? "yr" : "mo"}`
+ : ""}{" "}
+ ({data?.isAnnualCurrent ? "annual" : "monthly"})
+
+
+ New plan:{" "}
+ {upgradeTier === "startup"
+ ? "Startup"
+ : "Hobby"}{" "}
+ · {upgradeServerQty} server
+ {upgradeServerQty !== 1 ? "s" : ""} · $
+ {upgradeTier === "hobby"
+ ? calculatePriceHobby(
+ upgradeServerQty,
+ updateFormAnnual,
+ ).toFixed(2)
+ : calculatePriceStartup(
+ upgradeServerQty,
+ updateFormAnnual,
+ ).toFixed(2)}
+ /{updateFormAnnual ? "yr" : "mo"} (
+ {updateFormAnnual ? "annual" : "monthly"})
+
+
+ Stripe will prorate the change.
+
+
+ }
+ type="default"
+ onClick={async () => {
+ if (!upgradeTier) return;
+ try {
+ await upgradeSubscription({
+ tier: upgradeTier,
+ serverQuantity: upgradeServerQty,
+ isAnnual: updateFormAnnual,
+ });
+ await utils.stripe.getProducts.invalidate();
+
+ // add delay of 3 seconds
+ await new Promise((resolve) =>
+ setTimeout(resolve, 3000),
+ );
+ await utils.user.get.invalidate();
+ setUpgradeTier(null);
+ toast.success(
+ "Subscription updated successfully",
+ );
+ } catch {
+ toast.error("Error updating subscription");
+ }
+ }}
+ >
+
+ {isUpgrading ? (
+ <>
+
+ Updating…
+ >
+ ) : (
+ "Update subscription"
+ )}
+
+
+
+ )}
+
+ )}
Need Help? We are here to help you.
@@ -186,13 +643,350 @@ export const ShowBilling = () => {
- {isLoading ? (
+ {isPending ? (
Loading...
+ ) : useNewPricing ? (
+ <>
+ setIsAnnual(e === "annual")}
+ >
+
+ Monthly
+ Annual (20% off)
+
+
+
+ {/* Hobby */}
+
+ {isAnnual && (
+
+ 20% off
+
+ )}
+
+ Hobby
+
+
+ Everything an individual developer needs
+
+
+
+ $
+ {calculatePriceHobby(
+ serverQuantity,
+ isAnnual,
+ ).toFixed(2)}
+ /{isAnnual ? "yr" : "mo"}
+
+
+ Add more servers as you'd like for{" "}
+ {isAnnual ? "$43.20/yr" : "$4.50/mo"}
+
+ {isAnnual && (
+
+ $
+ {(
+ calculatePriceHobby(serverQuantity, true) / 12
+ ).toFixed(2)}
+ /mo
+
+ )}
+
+
+ {[
+ "Unlimited Deployments",
+ "Unlimited Databases",
+ "Unlimited Applications",
+ "1 Server Included",
+ "1 Organization",
+ "1 User",
+ "2 Environments",
+ "1 Volume Backup per Application",
+ "1 Backup per Database",
+ "1 Scheduled Job per Application",
+ "Community Support (Discord)",
+ ].map((f) => (
+
+
+ {f}
+
+ ))}
+
+
+
+
+ Servers:
+
+
+ setServerQuantity((q) => Math.max(1, q - 1))
+ }
+ >
+
+
+
+ setServerQuantity(
+ Math.max(
+ 1,
+ Number(
+ (e.target as HTMLInputElement).value,
+ ) || 1,
+ ),
+ )
+ }
+ className="text-center"
+ />
+ setServerQuantity((q) => q + 1)}
+ >
+
+
+
+
+ {admin?.user.stripeCustomerId && (
+ {
+ const session =
+ await createCustomerPortalSession();
+ window.open(session.url);
+ }}
+ >
+ Manage Subscription
+
+ )}
+ {(data?.subscriptions?.length ?? 0) === 0 && (
+
+ handleCheckout("hobby", data!.hobbyProductId!)
+ }
+ disabled={serverQuantity < 1}
+ >
+ Get Started
+
+ )}
+
+
+
+
+ {/* Startup - Recommended */}
+
+
+
+ Recommended
+
+ {isAnnual && (
+
+ 20% off
+
+ )}
+
+
+ Startup
+
+
+ Perfect for small to mid-size teams
+
+
+
+ $
+ {calculatePriceStartup(
+ serverQuantity,
+ isAnnual,
+ ).toFixed(2)}
+ /{isAnnual ? "yr" : "mo"}
+
+
+ Add more servers as you'd like for{" "}
+ {isAnnual ? "$43.20/yr" : "$4.50/mo"}
+
+ {isAnnual && (
+
+ $
+ {(
+ calculatePriceStartup(serverQuantity, true) / 12
+ ).toFixed(2)}
+ /mo
+
+ )}
+
+
+
+
+ All the features of Hobby, plus…
+
+ {[
+ "3 Servers Included",
+ "3 Organizations",
+ "Unlimited Users",
+ "Unlimited Environments",
+ "Unlimited Volume Backups",
+ "Unlimited Database Backups",
+ "Unlimited Scheduled Jobs",
+ "Basic RBAC (Admin, Developer)",
+ "2FA",
+ "Email and Chat Support",
+ ].map((f) => (
+
+
+ {f}
+
+ ))}
+
+
+
+
+ Servers (min. {STARTUP_SERVERS_INCLUDED} included)
+
+
+
+ setServerQuantity((q) =>
+ Math.max(STARTUP_SERVERS_INCLUDED, q - 1),
+ )
+ }
+ >
+
+
+
+ setServerQuantity(
+ Math.max(
+ STARTUP_SERVERS_INCLUDED,
+ Number(
+ (e.target as HTMLInputElement).value,
+ ) || STARTUP_SERVERS_INCLUDED,
+ ),
+ )
+ }
+ className="h-8 text-center"
+ />
+ setServerQuantity((q) => q + 1)}
+ >
+
+
+
+
+
+ {admin?.user.stripeCustomerId && (
+ {
+ const session =
+ await createCustomerPortalSession();
+ window.open(session.url);
+ }}
+ >
+ Manage Subscription
+
+ )}
+ {(data?.subscriptions?.length ?? 0) === 0 && (
+
+ handleCheckout(
+ "startup",
+ data!.startupProductId!,
+ )
+ }
+ disabled={
+ serverQuantity < STARTUP_SERVERS_INCLUDED
+ }
+ >
+ Get Started
+
+ )}
+
+
+
+
+ {/* Enterprise */}
+
+
+ Enterprise
+
+
+ For large organizations who want more control
+
+
+
+
+
+ All the features of Startup, plus…
+
+ {[
+ "Up to Unlimited Servers",
+ "Up to Unlimited Organizations",
+ "Fine-grained RBAC",
+ "Complete Hosting Flexibility",
+ "SSO / SAML (Azure, OKTA, etc)",
+ "Audit Logs",
+ "MSA/SLA",
+ "White Labeling",
+ "Priority Support and Services",
+ ].map((f) => (
+
+
+ {f}
+
+ ))}
+
+
+
+ Contact Sales
+
+
+
+
+ >
) : (
<>
+ setIsAnnual(e === "annual")}
+ >
+
+ Monthly
+ Annual (20% off)
+
+
{products?.map((product) => {
const featured = true;
return (
@@ -311,15 +1105,7 @@ export const ShowBilling = () => {
-
0
- ? "justify-between"
- : "justify-end",
- "flex flex-row items-center gap-2 mt-4",
- )}
- >
+
{admin?.user.stripeCustomerId && (
{
onClick={async () => {
const session =
await createCustomerPortalSession();
-
window.open(session.url);
}}
>
Manage Subscription
)}
-
- {data?.subscriptions?.length === 0 && (
-
- {
- handleCheckout(product.id);
- }}
- disabled={serverQuantity < 1}
- >
- Subscribe
-
-
+ {(data?.subscriptions?.length ?? 0) === 0 && (
+
{
+ handleCheckout("legacy", product.id);
+ }}
+ disabled={serverQuantity < 1}
+ >
+ Subscribe
+
)}
diff --git a/apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx b/apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx
index 73cc82efc..b10e09596 100644
--- a/apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx
+++ b/apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx
@@ -53,11 +53,11 @@ const getStatusBadge = (status: Stripe.Invoice.Status | null) => {
};
export const ShowInvoices = () => {
- const { data: invoices, isLoading } = api.stripe.getInvoices.useQuery();
+ const { data: invoices, isPending } = api.stripe.getInvoices.useQuery();
return (
- {isLoading ? (
+ {isPending ? (
Loading invoices...
diff --git a/apps/dokploy/components/dashboard/settings/billing/show-welcome-dokploy.tsx b/apps/dokploy/components/dashboard/settings/billing/show-welcome-dokploy.tsx
index f87ca58c7..ca1407d7e 100644
--- a/apps/dokploy/components/dashboard/settings/billing/show-welcome-dokploy.tsx
+++ b/apps/dokploy/components/dashboard/settings/billing/show-welcome-dokploy.tsx
@@ -12,7 +12,7 @@ export const ShowWelcomeDokploy = () => {
const { data } = api.user.get.useQuery();
const [open, setOpen] = useState(false);
- const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
+ const { data: isCloud, isPending } = api.settings.isCloud.useQuery();
if (!isCloud || data?.role !== "admin") {
return null;
@@ -20,14 +20,14 @@ export const ShowWelcomeDokploy = () => {
useEffect(() => {
if (
- !isLoading &&
+ !isPending &&
isCloud &&
!localStorage.getItem("hasSeenCloudWelcomeModal") &&
data?.role === "owner"
) {
setOpen(true);
}
- }, [isCloud, isLoading]);
+ }, [isCloud, isPending]);
const handleClose = (isOpen: boolean) => {
if (data?.role === "owner") {
diff --git a/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx b/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx
index 6f7ef6821..bc29a4c95 100644
--- a/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx
+++ b/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { HelpCircle, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -62,7 +62,7 @@ export const AddCertificate = () => {
const utils = api.useUtils();
const { data: isCloud } = api.settings.isCloud.useQuery();
- const { mutateAsync, isError, error, isLoading } =
+ const { mutateAsync, isError, error, isPending } =
api.certificates.create.useMutation();
const { data: servers } = api.server.withSSHKey.useQuery();
const hasServers = servers && servers.length > 0;
@@ -247,7 +247,7 @@ export const AddCertificate = () => {
diff --git a/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx b/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx
index 8356d89c6..e861c9027 100644
--- a/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx
+++ b/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx
@@ -15,9 +15,9 @@ import { AddCertificate } from "./add-certificate";
import { getCertificateChainInfo, getExpirationStatus } from "./utils";
export const ShowCertificates = () => {
- const { mutateAsync, isLoading: isRemoving } =
+ const { mutateAsync, isPending: isRemoving } =
api.certificates.remove.useMutation();
- const { data, isLoading, refetch } = api.certificates.all.useQuery();
+ const { data, isPending, refetch } = api.certificates.all.useQuery();
return (
@@ -40,7 +40,7 @@ export const ShowCertificates = () => {
- {isLoading ? (
+ {isPending ? (
Loading...
diff --git a/apps/dokploy/components/dashboard/settings/cluster/nodes/manager/add-manager.tsx b/apps/dokploy/components/dashboard/settings/cluster/nodes/manager/add-manager.tsx
index 36dda311c..a24e4dc2f 100644
--- a/apps/dokploy/components/dashboard/settings/cluster/nodes/manager/add-manager.tsx
+++ b/apps/dokploy/components/dashboard/settings/cluster/nodes/manager/add-manager.tsx
@@ -15,7 +15,7 @@ interface Props {
}
export const AddManager = ({ serverId }: Props) => {
- const { data, isLoading, error, isError } = api.cluster.addManager.useQuery({
+ const { data, isPending, error, isError } = api.cluster.addManager.useQuery({
serverId,
});
@@ -27,7 +27,7 @@ export const AddManager = ({ serverId }: Props) => {
Add a new manager
{isError &&
{error?.message} }
- {isLoading ? (
+ {isPending ? (
) : (
<>
diff --git a/apps/dokploy/components/dashboard/settings/cluster/nodes/show-nodes.tsx b/apps/dokploy/components/dashboard/settings/cluster/nodes/show-nodes.tsx
index b88fdd8e8..c7f580caa 100644
--- a/apps/dokploy/components/dashboard/settings/cluster/nodes/show-nodes.tsx
+++ b/apps/dokploy/components/dashboard/settings/cluster/nodes/show-nodes.tsx
@@ -48,7 +48,7 @@ interface Props {
}
export const ShowNodes = ({ serverId }: Props) => {
- const { data, isLoading, refetch } = api.cluster.getNodes.useQuery({
+ const { data, isPending, refetch } = api.cluster.getNodes.useQuery({
serverId,
});
const { data: registry } = api.registry.all.useQuery();
@@ -75,7 +75,7 @@ export const ShowNodes = ({ serverId }: Props) => {
)}
- {isLoading ? (
+ {isPending ? (
diff --git a/apps/dokploy/components/dashboard/settings/cluster/nodes/workers/add-worker.tsx b/apps/dokploy/components/dashboard/settings/cluster/nodes/workers/add-worker.tsx
index c73e458ef..40a673265 100644
--- a/apps/dokploy/components/dashboard/settings/cluster/nodes/workers/add-worker.tsx
+++ b/apps/dokploy/components/dashboard/settings/cluster/nodes/workers/add-worker.tsx
@@ -15,7 +15,7 @@ interface Props {
}
export const AddWorker = ({ serverId }: Props) => {
- const { data, isLoading, error, isError } = api.cluster.addWorker.useQuery({
+ const { data, isPending, error, isError } = api.cluster.addWorker.useQuery({
serverId,
});
@@ -26,7 +26,7 @@ export const AddWorker = ({ serverId }: Props) => {
Add a new worker
{isError && {error?.message} }
- {isLoading ? (
+ {isPending ? (
) : (
<>
diff --git a/apps/dokploy/components/dashboard/settings/cluster/registry/handle-registry.tsx b/apps/dokploy/components/dashboard/settings/cluster/registry/handle-registry.tsx
index 979276995..e22285c73 100644
--- a/apps/dokploy/components/dashboard/settings/cluster/registry/handle-registry.tsx
+++ b/apps/dokploy/components/dashboard/settings/cluster/registry/handle-registry.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { AlertTriangle, PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -105,13 +105,13 @@ export const HandleRegistry = ({ registryId }: Props) => {
const servers = [...(deployServers || []), ...(buildServers || [])];
const {
mutateAsync: testRegistry,
- isLoading,
+ isPending,
error: testRegistryError,
isError: testRegistryIsError,
} = api.registry.testRegistry.useMutation();
const {
mutateAsync: testRegistryById,
- isLoading: isLoadingById,
+ isPending: isPendingById,
error: testRegistryByIdError,
isError: testRegistryByIdIsError,
} = api.registry.testRegistryById.useMutation();
@@ -451,7 +451,7 @@ export const HandleRegistry = ({ registryId }: Props) => {
{
// When editing with empty password, use the existing password from DB
if (registryId && (!password || password.length === 0)) {
diff --git a/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx b/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx
index 62131fedd..f79c246b0 100644
--- a/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx
+++ b/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx
@@ -14,9 +14,9 @@ import { api } from "@/utils/api";
import { HandleRegistry } from "./handle-registry";
export const ShowRegistry = () => {
- const { mutateAsync, isLoading: isRemoving } =
+ const { mutateAsync, isPending: isRemoving } =
api.registry.remove.useMutation();
- const { data, isLoading, refetch } = api.registry.all.useQuery();
+ const { data, isPending, refetch } = api.registry.all.useQuery();
return (
diff --git a/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx b/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx
index dae069e91..966c8e5f5 100644
--- a/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx
+++ b/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -60,7 +60,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
const { data: servers } = api.server.withSSHKey.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
- const { mutateAsync, isError, error, isLoading } = destinationId
+ const { mutateAsync, isError, error, isPending } = destinationId
? api.destination.update.useMutation()
: api.destination.create.useMutation();
@@ -75,7 +75,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
);
const {
mutateAsync: testConnection,
- isLoading: isLoadingConnection,
+ isPending: isPendingConnection,
error: connectionError,
isError: isErrorConnection,
} = api.destination.testConnection.useMutation();
@@ -410,7 +410,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
{
await handleTestConnection(form.getValues("serverId"));
}}
@@ -420,7 +420,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
) : (
{
@@ -432,7 +432,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
)}
diff --git a/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx b/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx
index f0ad39807..3cb29d54a 100644
--- a/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx
+++ b/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx
@@ -13,8 +13,8 @@ import { api } from "@/utils/api";
import { HandleDestinations } from "./handle-destinations";
export const ShowDestinations = () => {
- const { data, isLoading, refetch } = api.destination.all.useQuery();
- const { mutateAsync, isLoading: isRemoving } =
+ const { data, isPending, refetch } = api.destination.all.useQuery();
+ const { mutateAsync, isPending: isRemoving } =
api.destination.remove.useMutation();
return (
@@ -31,7 +31,7 @@ export const ShowDestinations = () => {
- {isLoading ? (
+ {isPending ? (
Loading...
diff --git a/apps/dokploy/components/dashboard/settings/git/bitbucket/add-bitbucket-provider.tsx b/apps/dokploy/components/dashboard/settings/git/bitbucket/add-bitbucket-provider.tsx
index c933a0b8c..51a2291c1 100644
--- a/apps/dokploy/components/dashboard/settings/git/bitbucket/add-bitbucket-provider.tsx
+++ b/apps/dokploy/components/dashboard/settings/git/bitbucket/add-bitbucket-provider.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { ExternalLink } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
diff --git a/apps/dokploy/components/dashboard/settings/git/bitbucket/edit-bitbucket-provider.tsx b/apps/dokploy/components/dashboard/settings/git/bitbucket/edit-bitbucket-provider.tsx
index 3cccdff71..045b0196f 100644
--- a/apps/dokploy/components/dashboard/settings/git/bitbucket/edit-bitbucket-provider.tsx
+++ b/apps/dokploy/components/dashboard/settings/git/bitbucket/edit-bitbucket-provider.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -58,7 +58,7 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, error, isError } = api.bitbucket.update.useMutation();
- const { mutateAsync: testConnection, isLoading } =
+ const { mutateAsync: testConnection, isPending } =
api.bitbucket.testConnection.useMutation();
const form = useForm
({
defaultValues: {
@@ -257,7 +257,7 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
{
await testConnection({
bitbucketId,
diff --git a/apps/dokploy/components/dashboard/settings/git/gitea/add-gitea-provider.tsx b/apps/dokploy/components/dashboard/settings/git/gitea/add-gitea-provider.tsx
index f474c376d..89307b1ce 100644
--- a/apps/dokploy/components/dashboard/settings/git/gitea/add-gitea-provider.tsx
+++ b/apps/dokploy/components/dashboard/settings/git/gitea/add-gitea-provider.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { ExternalLink } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
@@ -19,9 +19,9 @@ import {
import {
Form,
FormControl,
+ FormDescription,
FormField,
FormItem,
- FormDescription,
FormLabel,
FormMessage,
} from "@/components/ui/form";
@@ -68,7 +68,7 @@ export const AddGiteaProvider = () => {
const { mutateAsync, error, isError } = api.gitea.create.useMutation();
const webhookUrl = `${baseUrl}/api/providers/gitea/callback`;
- const form = useForm({
+ const form = useForm({
defaultValues: {
clientId: "",
clientSecret: "",
diff --git a/apps/dokploy/components/dashboard/settings/git/gitea/edit-gitea-provider.tsx b/apps/dokploy/components/dashboard/settings/git/gitea/edit-gitea-provider.tsx
index fe578acce..b59687004 100644
--- a/apps/dokploy/components/dashboard/settings/git/gitea/edit-gitea-provider.tsx
+++ b/apps/dokploy/components/dashboard/settings/git/gitea/edit-gitea-provider.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
@@ -17,9 +17,9 @@ import {
import {
Form,
FormControl,
+ FormDescription,
FormField,
FormItem,
- FormDescription,
FormLabel,
FormMessage,
} from "@/components/ui/form";
@@ -51,8 +51,8 @@ export const EditGiteaProvider = ({ giteaId }: Props) => {
isLoading,
refetch,
} = api.gitea.one.useQuery({ giteaId });
- const { mutateAsync, isLoading: isUpdating } = api.gitea.update.useMutation();
- const { mutateAsync: testConnection, isLoading: isTesting } =
+ const { mutateAsync, isPending: isUpdating } = api.gitea.update.useMutation();
+ const { mutateAsync: testConnection, isPending: isTesting } =
api.gitea.testConnection.useMutation();
const url = useUrl();
const utils = api.useUtils();
@@ -94,7 +94,7 @@ export const EditGiteaProvider = ({ giteaId }: Props) => {
}
}, [router.query, router.isReady, refetch]);
- const form = useForm>({
+ const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
diff --git a/apps/dokploy/components/dashboard/settings/git/github/add-github-provider.tsx b/apps/dokploy/components/dashboard/settings/git/github/add-github-provider.tsx
index 010ae25e7..510a0c7f6 100644
--- a/apps/dokploy/components/dashboard/settings/git/github/add-github-provider.tsx
+++ b/apps/dokploy/components/dashboard/settings/git/github/add-github-provider.tsx
@@ -30,7 +30,7 @@ export const AddGithubProvider = () => {
const url = document.location.origin;
const manifest = JSON.stringify(
{
- redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id}&userId=${session?.user?.id}`,
+ redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id ?? ""}&userId=${session?.user?.id ?? ""}`,
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}-${randomString()}`,
url: origin,
hook_attributes: {
@@ -52,7 +52,7 @@ export const AddGithubProvider = () => {
);
setManifest(manifest);
- }, [data?.id]);
+ }, [data?.id, activeOrganization?.id, session?.user?.id]);
return (
@@ -131,7 +131,11 @@ export const AddGithubProvider = () => {
Unsure if you already have an app?
diff --git a/apps/dokploy/components/dashboard/settings/git/github/edit-github-provider.tsx b/apps/dokploy/components/dashboard/settings/git/github/edit-github-provider.tsx
index 62463d0dc..7aeb565c1 100644
--- a/apps/dokploy/components/dashboard/settings/git/github/edit-github-provider.tsx
+++ b/apps/dokploy/components/dashboard/settings/git/github/edit-github-provider.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -53,7 +53,7 @@ export const EditGithubProvider = ({ githubId }: Props) => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, error, isError } = api.github.update.useMutation();
- const { mutateAsync: testConnection, isLoading } =
+ const { mutateAsync: testConnection, isPending } =
api.github.testConnection.useMutation();
const form = useForm({
defaultValues: {
@@ -151,7 +151,7 @@ export const EditGithubProvider = ({ githubId }: Props) => {
{
await testConnection({
githubId,
diff --git a/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx b/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx
index 69d926194..7c637f5ef 100644
--- a/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx
+++ b/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { ExternalLink } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
@@ -19,9 +19,9 @@ import {
import {
Form,
FormControl,
+ FormDescription,
FormField,
FormItem,
- FormDescription,
FormLabel,
FormMessage,
} from "@/components/ui/form";
@@ -63,7 +63,7 @@ export const AddGitlabProvider = () => {
const { mutateAsync, error, isError } = api.gitlab.create.useMutation();
const webhookUrl = `${url}/api/providers/gitlab/callback`;
- const form = useForm({
+ const form = useForm({
defaultValues: {
applicationId: "",
applicationSecret: "",
diff --git a/apps/dokploy/components/dashboard/settings/git/gitlab/edit-gitlab-provider.tsx b/apps/dokploy/components/dashboard/settings/git/gitlab/edit-gitlab-provider.tsx
index 394e25281..e48df084b 100644
--- a/apps/dokploy/components/dashboard/settings/git/gitlab/edit-gitlab-provider.tsx
+++ b/apps/dokploy/components/dashboard/settings/git/gitlab/edit-gitlab-provider.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -18,9 +18,9 @@ import {
import {
Form,
FormControl,
+ FormDescription,
FormField,
FormItem,
- FormDescription,
FormLabel,
FormMessage,
} from "@/components/ui/form";
@@ -59,9 +59,9 @@ export const EditGitlabProvider = ({ gitlabId }: Props) => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, error, isError } = api.gitlab.update.useMutation();
- const { mutateAsync: testConnection, isLoading } =
+ const { mutateAsync: testConnection, isPending } =
api.gitlab.testConnection.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
groupName: "",
name: "",
@@ -205,7 +205,7 @@ export const EditGitlabProvider = ({ gitlabId }: Props) => {
{
await testConnection({
gitlabId,
diff --git a/apps/dokploy/components/dashboard/settings/git/show-git-providers.tsx b/apps/dokploy/components/dashboard/settings/git/show-git-providers.tsx
index 5e4c12696..a96bcf26c 100644
--- a/apps/dokploy/components/dashboard/settings/git/show-git-providers.tsx
+++ b/apps/dokploy/components/dashboard/settings/git/show-git-providers.tsx
@@ -36,8 +36,8 @@ import { AddGitlabProvider } from "./gitlab/add-gitlab-provider";
import { EditGitlabProvider } from "./gitlab/edit-gitlab-provider";
export const ShowGitProviders = () => {
- const { data, isLoading, refetch } = api.gitProvider.getAll.useQuery();
- const { mutateAsync, isLoading: isRemoving } =
+ const { data, isPending, refetch } = api.gitProvider.getAll.useQuery();
+ const { mutateAsync, isPending: isRemoving } =
api.gitProvider.remove.useMutation();
const url = useUrl();
@@ -66,7 +66,7 @@ export const ShowGitProviders = () => {
- {isLoading ? (
+ {isPending ? (
Loading...
diff --git a/apps/dokploy/components/dashboard/settings/handle-ai.tsx b/apps/dokploy/components/dashboard/settings/handle-ai.tsx
index b5a0d51d8..d600d3a8e 100644
--- a/apps/dokploy/components/dashboard/settings/handle-ai.tsx
+++ b/apps/dokploy/components/dashboard/settings/handle-ai.tsx
@@ -1,5 +1,5 @@
"use client";
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Check, ChevronDown, PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -57,7 +57,6 @@ interface Props {
export const HandleAi = ({ aiId }: Props) => {
const utils = api.useUtils();
- const [error, setError] = useState
(null);
const [open, setOpen] = useState(false);
const [modelPopoverOpen, setModelPopoverOpen] = useState(false);
const [modelSearch, setModelSearch] = useState("");
@@ -69,7 +68,7 @@ export const HandleAi = ({ aiId }: Props) => {
enabled: !!aiId,
},
);
- const { mutateAsync, isLoading } = aiId
+ const { mutateAsync, isPending } = aiId
? api.ai.update.useMutation()
: api.ai.create.useMutation();
@@ -102,19 +101,19 @@ export const HandleAi = ({ aiId }: Props) => {
const apiKey = form.watch("apiKey");
const isOllama = apiUrl.includes(":11434") || apiUrl.includes("ollama");
- const { data: models, isLoading: isLoadingServerModels } =
- api.ai.getModels.useQuery(
- {
- apiUrl: apiUrl ?? "",
- apiKey: apiKey ?? "",
- },
- {
- enabled: !!apiUrl && (isOllama || !!apiKey),
- onError: (error) => {
- setError(`Failed to fetch models: ${error.message}`);
- },
- },
- );
+ const {
+ data: models,
+ isPending: isLoadingServerModels,
+ error: modelsError,
+ } = api.ai.getModels.useQuery(
+ {
+ apiUrl: apiUrl ?? "",
+ apiKey: apiKey ?? "",
+ },
+ {
+ enabled: !!apiUrl && (isOllama || !!apiKey),
+ },
+ );
const onSubmit = async (data: Schema) => {
try {
@@ -169,7 +168,9 @@ export const HandleAi = ({ aiId }: Props) => {