Compare commits

...

54 Commits

Author SHA1 Message Date
Mauricio Siu
2619cb49d1 refactor: restore commented-out test cases and imports in drop.test.test.ts for improved functionality
Some checks failed
Auto PR to main when version changes / create-pr (push) Has been cancelled
Build Docker images / build-and-push-cloud-image (push) Has been cancelled
Build Docker images / build-and-push-schedule-image (push) Has been cancelled
Build Docker images / build-and-push-server-image (push) Has been cancelled
Dokploy Docker Build / docker-amd (push) Has been cancelled
Dokploy Docker Build / docker-arm (push) Has been cancelled
autofix.ci / format (push) Has been cancelled
Dokploy Monitoring Build / docker-amd (push) Has been cancelled
Dokploy Monitoring Build / docker-arm (push) Has been cancelled
Dokploy Docker Build / combine-manifests (push) Has been cancelled
Dokploy Docker Build / generate-release (push) Has been cancelled
Dokploy Monitoring Build / combine-manifests (push) Has been cancelled
2025-05-28 02:44:06 -06:00
Mauricio Siu
46d12fa9d8 Merge pull request #1967 from Dokploy/feat/add-chatwoot
Feat/add chatwoot
2025-05-28 02:41:45 -06:00
Mauricio Siu
51ee46496c chore: update pnpm lockfile with dependency version upgrades for improved stability and compatibility 2025-05-28 02:39:18 -06:00
Mauricio Siu
a13e24dab0 refactor: simplify Chatwoot widget condition in dashboard layout for improved readability 2025-05-28 02:33:44 -06:00
Mauricio Siu
4aac3476b6 refactor: update Chatwoot widget settings and types to enhance configuration options 2025-05-28 02:33:14 -06:00
Mauricio Siu
037343a796 feat: integrate Chatwoot widget into dashboard layout and replace project layout with dashboard layout in various pages 2025-05-28 02:22:56 -06:00
Mauricio Siu
274d80ea7c refactor: comment out test cases and imports in drop.test.test.ts for cleanup 2025-05-28 00:51:20 -06:00
Mauricio Siu
629889f1a8 refactor: reorganize imports and enhance backup functionality across various components 2025-05-28 00:38:53 -06:00
Mauricio Siu
3e74ce05a7 Merge pull request #1960 from Lux1L/feature/gitlab-subgroup-filtering
feat(gitlab): support nested group filtering using namespace.full_pat…
2025-05-28 00:37:05 -06:00
Mauricio Siu
d05218e848 Merge pull request #1958 from IPdotSetAF/git-lfs-fix
fix: moved git lfs from build stage to dokploy stage in dockerfile
2025-05-28 00:36:12 -06:00
Mauricio Siu
0fbad4f75e docs: remove supported OS section from README.md 2025-05-28 00:34:47 -06:00
IPdotSetAF
c3cbaf2a57 fix: moved git lfs from build stage to dokploy stage in dockerfile 2025-05-27 22:16:46 +03:30
avalolu
560d493d56 feat(gitlab): support nested group filtering using namespace.full_path.startsWith 2025-05-26 18:00:03 -04:00
Mauricio Siu
27b2106630 chore: bump version to v0.22.7 in package.json
Some checks failed
Auto PR to main when version changes / create-pr (push) Has been cancelled
Build Docker images / build-and-push-cloud-image (push) Has been cancelled
Build Docker images / build-and-push-schedule-image (push) Has been cancelled
Build Docker images / build-and-push-server-image (push) Has been cancelled
Dokploy Docker Build / docker-amd (push) Has been cancelled
Dokploy Docker Build / docker-arm (push) Has been cancelled
autofix.ci / format (push) Has been cancelled
Dokploy Monitoring Build / docker-amd (push) Has been cancelled
Dokploy Monitoring Build / docker-arm (push) Has been cancelled
Dokploy Docker Build / combine-manifests (push) Has been cancelled
Dokploy Docker Build / generate-release (push) Has been cancelled
Dokploy Monitoring Build / combine-manifests (push) Has been cancelled
2025-05-26 03:11:23 -06:00
Mauricio Siu
609954c366 Merge pull request #1931 from nktnet1/nginx-static-spa-build
feat: added SPA option for static sites
2025-05-26 03:10:51 -06:00
Mauricio Siu
84faa9747e chore: update Node.js version to 20.16.0 in configuration files 2025-05-26 03:09:09 -06:00
Mauricio Siu
4b370ef43e chore: update otpauth version to 9.4.0 in pnpm-lock.yaml 2025-05-26 03:01:57 -06:00
Mauricio Siu
b94a6bff92 Merge branch 'canary' into nginx-static-spa-build 2025-05-26 02:59:03 -06:00
Mauricio Siu
276b754377 chore: downgrade docker/build-push-action to version 4 in deploy workflows 2025-05-26 02:27:41 -06:00
Mauricio Siu
f3b3798362 chore: update docker/build-push-action to version 6 in deploy workflows 2025-05-26 02:15:08 -06:00
Mauricio Siu
461acc354e Merge pull request #1955 from Dokploy/1923-v0226-git-lfs-is-not-working-at-all-despite-1872
chore: add git-lfs to Dockerfile for large file support
2025-05-26 02:00:50 -06:00
Mauricio Siu
dfc75a9116 chore: remove Dockerfile for dokploy as part of project restructuring 2025-05-26 01:53:24 -06:00
Mauricio Siu
e1580bad23 chore: add git-lfs to Dockerfile for large file support 2025-05-26 01:52:41 -06:00
Mauricio Siu
b567ec1d83 Merge pull request #1954 from Dokploy/1943-error-backing-up-mysql-to-cloudflare-r2
feat: add pino and pino-pretty for logging, implement logger utility
2025-05-26 01:48:35 -06:00
Mauricio Siu
9c73b8dc36 feat: add pino and pino-pretty for logging, implement logger utility 2025-05-26 01:45:14 -06:00
Mauricio Siu
7348526873 Merge pull request #1953 from Dokploy/1898-remote-server-with-ipv6
fix: update slugIp formatting to handle colons in server IP
2025-05-26 00:55:43 -06:00
Mauricio Siu
6fc83f2db3 fix: update slugIp formatting to handle colons in server IP 2025-05-26 00:55:22 -06:00
Khiet Tam Nguyen
43d22c2bd4 test: fix typescript error for isStaticSpa 2025-05-20 16:33:34 +10:00
autofix-ci[bot]
38a5313967 [autofix.ci] apply automated fixes 2025-05-20 06:18:00 +00:00
Khiet Tam Nguyen
ba3645933f feat: added SPA option for static sites 2025-05-20 16:11:48 +10:00
Mauricio Siu
17a26353b6 chore: bump version to v0.22.6 in package.json
Some checks failed
Auto PR to main when version changes / create-pr (push) Has been cancelled
Build Docker images / build-and-push-cloud-image (push) Has been cancelled
Build Docker images / build-and-push-schedule-image (push) Has been cancelled
Build Docker images / build-and-push-server-image (push) Has been cancelled
Dokploy Docker Build / docker-amd (push) Has been cancelled
Dokploy Docker Build / docker-arm (push) Has been cancelled
autofix.ci / format (push) Has been cancelled
Dokploy Monitoring Build / docker-amd (push) Has been cancelled
Dokploy Monitoring Build / docker-arm (push) Has been cancelled
Dokploy Docker Build / combine-manifests (push) Has been cancelled
Dokploy Docker Build / generate-release (push) Has been cancelled
Dokploy Monitoring Build / combine-manifests (push) Has been cancelled
2025-05-18 02:30:04 -06:00
Mauricio Siu
e2c163c6d5 Merge pull request #1919 from nktnet1/fix-randomise-compose-await
fix: randomize-compose missing await
2025-05-18 02:29:32 -06:00
Khiet Tam Nguyen
616e11722c fix: randomize-compose missing await 2025-05-18 18:26:44 +10:00
Mauricio Siu
91a44706df Merge pull request #1917 from nktnet1/fix-isolated-randomized-compose-notifs
fix: multiple notifications for isolated compose and randomize compose
2025-05-18 02:19:51 -06:00
Mauricio Siu
748de47a6d Merge pull request #1918 from Dokploy/fix/web-server-backup-maxlenght
fix: update rsync command in web server backup to remove verbose flag
2025-05-18 02:18:31 -06:00
Mauricio Siu
cbf9aef0df fix: remove console log for rsync command in web server backup 2025-05-18 02:18:05 -06:00
Mauricio Siu
e2befc24a5 fix: update rsync command in web server backup to remove verbose flag 2025-05-18 02:17:41 -06:00
autofix-ci[bot]
0f48f2c830 [autofix.ci] apply automated fixes 2025-05-18 05:12:06 +00:00
Khiet Tam Nguyen
5dfa7645f3 fix: multiple notifications for isolated compose and randomize compose 2025-05-18 15:07:05 +10:00
Mauricio Siu
7fe163dd33 Merge pull request #1913 from Dokploy/feat/add-appname-compose
Some checks are pending
Auto PR to main when version changes / create-pr (push) Waiting to run
Build Docker images / build-and-push-cloud-image (push) Waiting to run
Build Docker images / build-and-push-schedule-image (push) Waiting to run
Build Docker images / build-and-push-server-image (push) Waiting to run
Dokploy Docker Build / docker-amd (push) Waiting to run
Dokploy Docker Build / docker-arm (push) Waiting to run
Dokploy Docker Build / combine-manifests (push) Blocked by required conditions
Dokploy Docker Build / generate-release (push) Blocked by required conditions
autofix.ci / format (push) Waiting to run
Dokploy Monitoring Build / docker-amd (push) Waiting to run
Dokploy Monitoring Build / docker-arm (push) Waiting to run
Dokploy Monitoring Build / combine-manifests (push) Blocked by required conditions
Feat/add appname compose
2025-05-17 15:37:22 -06:00
Mauricio Siu
19b56771b8 style: update styling for environment display and increase scroll area height in import component 2025-05-17 15:28:52 -06:00
Mauricio Siu
cff01ed438 refactor: modify template processing to include APP_NAME variable in configuration 2025-05-17 15:27:42 -06:00
Mauricio Siu
10fa3c8cf1 fix: update environment file generation to include APP_NAME variable 2025-05-17 15:18:31 -06:00
Mauricio Siu
6c5497ed21 Merge pull request #1912 from Dokploy/1873-duplicate-clones-the-project-not-the-service
feat: enhance project duplication functionality with options for new …
2025-05-17 14:35:17 -06:00
Mauricio Siu
380656efee feat: enhance project duplication functionality with options for new or same project 2025-05-17 14:33:07 -06:00
Mauricio Siu
c64d2245ce Merge pull request #1897 from enie123/canary
Some checks are pending
Auto PR to main when version changes / create-pr (push) Waiting to run
Build Docker images / build-and-push-cloud-image (push) Waiting to run
Build Docker images / build-and-push-schedule-image (push) Waiting to run
Build Docker images / build-and-push-server-image (push) Waiting to run
Dokploy Docker Build / docker-amd (push) Waiting to run
Dokploy Docker Build / docker-arm (push) Waiting to run
Dokploy Docker Build / combine-manifests (push) Blocked by required conditions
Dokploy Docker Build / generate-release (push) Blocked by required conditions
autofix.ci / format (push) Waiting to run
Dokploy Monitoring Build / docker-amd (push) Waiting to run
Dokploy Monitoring Build / docker-arm (push) Waiting to run
Dokploy Monitoring Build / combine-manifests (push) Blocked by required conditions
build: update nixpacks to 1.39.0
2025-05-17 03:54:11 -06:00
Mauricio Siu
a985998b93 feat: add VPS provider recommendations and alerts in server settings 2025-05-17 03:53:37 -06:00
Mauricio Siu
4f3ba16dfa chore: bump version to v0.22.5 in package.json 2025-05-17 03:34:31 -06:00
Mauricio Siu
6c788429f1 Merge pull request #1910 from Dokploy/1894-gitlab-self-hosted-cannot-find-all-repository
refactor: streamline GitLab repository fetching by introducing valida…
2025-05-17 03:02:26 -06:00
Mauricio Siu
3176a9d7e3 refactor: streamline GitLab repository fetching by introducing validateGitlabProvider function for improved error handling and pagination 2025-05-17 02:51:40 -06:00
Mauricio Siu
94a6a9587e Merge pull request #1872 from IPdotSetAF/git-lfs-not-supported
fix: installed git-lfs in docker image
2025-05-17 02:09:03 -06:00
Mauricio Siu
911681f389 fix: add git-lfs installation to various OS setups in server-setup script 2025-05-17 02:03:04 -06:00
Eric Nie
d4032f34bf build: update nixpacks to 1.39.0 2025-05-15 00:45:48 +07:00
IPdotSetAF
5156b45ffc fix: installed git-lfs in docker image 2025-05-11 10:36:52 +03:30
95 changed files with 9265 additions and 3898 deletions

View File

@@ -12,7 +12,7 @@ jobs:
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20.9.0 node-version: 20.16.0
cache: "pnpm" cache: "pnpm"
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm run server:build - run: pnpm run server:build
@@ -26,7 +26,7 @@ jobs:
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20.9.0 node-version: 20.16.0
cache: "pnpm" cache: "pnpm"
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm run server:build - run: pnpm run server:build
@@ -39,7 +39,7 @@ jobs:
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20.9.0 node-version: 20.16.0
cache: "pnpm" cache: "pnpm"
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm run server:build - run: pnpm run server:build

2
.nvmrc
View File

@@ -1 +1 @@
20.9.0 20.16.0

View File

@@ -52,7 +52,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. 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.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory. 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.
```bash ```bash
git clone https://github.com/dokploy/dokploy.git git clone https://github.com/dokploy/dokploy.git

View File

@@ -29,7 +29,7 @@ WORKDIR /app
# Set production # Set production
ENV NODE_ENV=production ENV NODE_ENV=production
RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 rsync && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 rsync git-lfs && git lfs install && rm -rf /var/lib/apt/lists/*
# Copy only the necessary files # Copy only the necessary files
COPY --from=build /prod/dokploy/.next ./.next COPY --from=build /prod/dokploy/.next ./.next
@@ -49,7 +49,7 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm
# Install Nixpacks and tsx # Install Nixpacks and tsx
# | VERBOSE=1 VERSION=1.21.0 bash # | VERBOSE=1 VERSION=1.21.0 bash
ARG NIXPACKS_VERSION=1.35.0 ARG NIXPACKS_VERSION=1.39.0
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& chmod +x install.sh \ && chmod +x install.sh \
&& ./install.sh \ && ./install.sh \
@@ -63,4 +63,4 @@ RUN curl -sSL https://railpack.com/install.sh | bash
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
EXPOSE 3000 EXPOSE 3000
CMD [ "pnpm", "start" ] CMD [ "pnpm", "start" ]

View File

@@ -148,19 +148,6 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400" style="border-radius:20px;"/> <img src="https://dokploy.com/banner.png" alt="Watch the video" width="400" style="border-radius:20px;"/>
</a> </a>
<!-- ## Supported OS
- Ubuntu 24.04 LTS
- Ubuntu 23.10
- Ubuntu 22.04 LTS
- Ubuntu 20.04 LTS
- Ubuntu 18.04 LTS
- Debian 12
- Debian 11
- Fedora 40
- Centos 9
- Centos 8 -->
## Contributing ## Contributing
Check out the [Contributing Guide](CONTRIBUTING.md) for more information. Check out the [Contributing Guide](CONTRIBUTING.md) for more information.

View File

@@ -1 +1 @@
20.9.0 20.16.0

View File

@@ -1,26 +0,0 @@
FROM node:18-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM base AS build
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
# Install dependencies
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
# Build only the dokploy app
RUN pnpm run dokploy:build
# Deploy only the dokploy app
RUN pnpm deploy --filter=dokploy --prod /prod/dokploy
FROM base AS dokploy
COPY --from=build /prod/dokploy /prod/dokploy
WORKDIR /prod/dokploy
EXPOSE 3000
CMD [ "pnpm", "start" ]

View File

@@ -105,6 +105,7 @@ const baseApp: ApplicationNested = {
ports: [], ports: [],
projectId: "", projectId: "",
publishDirectory: null, publishDirectory: null,
isStaticSpa: null,
redirects: [], redirects: [],
refreshToken: "", refreshToken: "",
registry: null, registry: null,
@@ -149,67 +150,68 @@ describe("unzipDrop using real zip files", () => {
} finally { } finally {
} }
}); });
it("should correctly extract a zip with a single root folder and a subfolder", async () => {
baseApp.appName = "folderwithfile";
// const appName = "folderwithfile";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
});
it("should correctly extract a zip with multiple root folders", async () => {
baseApp.appName = "two-folders";
// const appName = "two-folders";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "folder1")).toBe(true);
expect(files.some((f) => f.name === "folder2")).toBe(true);
});
it("should correctly extract a zip with a single root with a file", async () => {
baseApp.appName = "nested";
// const appName = "nested";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/nested.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "folder1")).toBe(true);
expect(files.some((f) => f.name === "folder2")).toBe(true);
expect(files.some((f) => f.name === "folder3")).toBe(true);
});
it("should correctly extract a zip with a single root with a folder", async () => {
baseApp.appName = "folder-with-sibling-file";
// const appName = "folder-with-sibling-file";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "folder1")).toBe(true);
expect(files.some((f) => f.name === "test.txt")).toBe(true);
});
}); });
// it("should correctly extract a zip with a single root folder and a subfolder", async () => {
// baseApp.appName = "folderwithfile";
// // const appName = "folderwithfile";
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
// const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
// const zipBuffer = zip.toBuffer();
// const file = new File([zipBuffer], "single.zip");
// await unzipDrop(file, baseApp);
// const files = await fs.readdir(outputPath, { withFileTypes: true });
// expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
// });
// it("should correctly extract a zip with multiple root folders", async () => {
// baseApp.appName = "two-folders";
// // const appName = "two-folders";
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
// const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
// const zipBuffer = zip.toBuffer();
// const file = new File([zipBuffer], "single.zip");
// await unzipDrop(file, baseApp);
// const files = await fs.readdir(outputPath, { withFileTypes: true });
// expect(files.some((f) => f.name === "folder1")).toBe(true);
// expect(files.some((f) => f.name === "folder2")).toBe(true);
// });
// it("should correctly extract a zip with a single root with a file", async () => {
// baseApp.appName = "nested";
// // const appName = "nested";
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
// const zip = new AdmZip("./__test__/drop/zips/nested.zip");
// const zipBuffer = zip.toBuffer();
// const file = new File([zipBuffer], "single.zip");
// await unzipDrop(file, baseApp);
// const files = await fs.readdir(outputPath, { withFileTypes: true });
// expect(files.some((f) => f.name === "folder1")).toBe(true);
// expect(files.some((f) => f.name === "folder2")).toBe(true);
// expect(files.some((f) => f.name === "folder3")).toBe(true);
// });
// it("should correctly extract a zip with a single root with a folder", async () => {
// baseApp.appName = "folder-with-sibling-file";
// // const appName = "folder-with-sibling-file";
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
// const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
// const zipBuffer = zip.toBuffer();
// const file = new File([zipBuffer], "single.zip");
// await unzipDrop(file, baseApp);
// const files = await fs.readdir(outputPath, { withFileTypes: true });
// expect(files.some((f) => f.name === "folder1")).toBe(true);
// expect(files.some((f) => f.name === "test.txt")).toBe(true);
// });
// });

View File

@@ -85,6 +85,7 @@ const baseApp: ApplicationNested = {
ports: [], ports: [],
projectId: "", projectId: "",
publishDirectory: null, publishDirectory: null,
isStaticSpa: null,
redirects: [], redirects: [],
refreshToken: "", refreshToken: "",
registry: null, registry: null,

View File

@@ -1,5 +1,5 @@
import { describe, expect, test } from "vitest";
import { normalizeS3Path } from "@dokploy/server/utils/backups/utils"; import { normalizeS3Path } from "@dokploy/server/utils/backups/utils";
import { describe, expect, test } from "vitest";
describe("normalizeS3Path", () => { describe("normalizeS3Path", () => {
test("should handle empty and whitespace-only prefix", () => { test("should handle empty and whitespace-only prefix", () => {

View File

@@ -263,7 +263,7 @@ export const ShowImport = ({ composeId }: Props) => {
{templateInfo.template.envs.map((env, index) => ( {templateInfo.template.envs.map((env, index) => (
<div <div
key={index} key={index}
className="rounded-lg border bg-card p-2 font-mono text-sm" className="rounded-lg truncate border bg-card p-2 font-mono text-sm"
> >
{env} {env}
</div> </div>
@@ -328,7 +328,7 @@ export const ShowImport = ({ composeId }: Props) => {
<DialogDescription>Mount File Content</DialogDescription> <DialogDescription>Mount File Content</DialogDescription>
</DialogHeader> </DialogHeader>
<ScrollArea className="h-[25vh] pr-4"> <ScrollArea className="h-[45vh] pr-4">
<CodeEditor <CodeEditor
language="yaml" language="yaml"
value={selectedMount?.content || ""} value={selectedMount?.content || ""}

View File

@@ -2,6 +2,7 @@ import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { import {
Form, Form,
FormControl, FormControl,
@@ -63,10 +64,11 @@ const mySchema = z.discriminatedUnion("buildType", [
publishDirectory: z.string().optional(), publishDirectory: z.string().optional(),
}), }),
z.object({ z.object({
buildType: z.literal(BuildType.static), buildType: z.literal(BuildType.railpack),
}), }),
z.object({ z.object({
buildType: z.literal(BuildType.railpack), buildType: z.literal(BuildType.static),
isStaticSpa: z.boolean().default(false),
}), }),
]); ]);
@@ -83,6 +85,7 @@ interface ApplicationData {
dockerBuildStage?: string | null; dockerBuildStage?: string | null;
herokuVersion?: string | null; herokuVersion?: string | null;
publishDirectory?: string | null; publishDirectory?: string | null;
isStaticSpa?: boolean | null;
} }
function isValidBuildType(value: string): value is BuildType { function isValidBuildType(value: string): value is BuildType {
@@ -115,16 +118,18 @@ const resetData = (data: ApplicationData): AddTemplate => {
case BuildType.static: case BuildType.static:
return { return {
buildType: BuildType.static, buildType: BuildType.static,
isStaticSpa: data.isStaticSpa ?? false,
}; };
case BuildType.railpack: case BuildType.railpack:
return { return {
buildType: BuildType.railpack, buildType: BuildType.railpack,
}; };
default: default: {
const buildType = data.buildType as BuildType; const buildType = data.buildType as BuildType;
return { return {
buildType, buildType,
} as AddTemplate; } as AddTemplate;
}
} }
}; };
@@ -174,6 +179,8 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
data.buildType === BuildType.heroku_buildpacks data.buildType === BuildType.heroku_buildpacks
? data.herokuVersion ? data.herokuVersion
: null, : null,
isStaticSpa:
data.buildType === BuildType.static ? data.isStaticSpa : null,
}) })
.then(async () => { .then(async () => {
toast.success("Build type saved"); toast.success("Build type saved");
@@ -364,6 +371,30 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
)} )}
/> />
)} )}
{buildType === BuildType.static && (
<FormField
control={form.control}
name="isStaticSpa"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex items-center gap-x-2 p-2">
<Checkbox
id="checkboxIsStaticSpa"
value={String(field.value)}
checked={field.value}
onCheckedChange={field.onChange}
/>
<FormLabel htmlFor="checkboxIsStaticSpa">
Single Page Application (SPA)
</FormLabel>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button isLoading={isLoading} type="submit"> <Button isLoading={isLoading} type="submit">
Save Save

View File

@@ -1,5 +1,6 @@
import { DateTooltip } from "@/components/shared/date-tooltip"; import { DateTooltip } from "@/components/shared/date-tooltip";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Card, Card,
@@ -9,12 +10,11 @@ import {
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { type RouterOutputs, api } from "@/utils/api"; import { type RouterOutputs, api } from "@/utils/api";
import { RocketIcon, Clock, Loader2 } from "lucide-react"; import { Clock, Loader2, RocketIcon } from "lucide-react";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { CancelQueues } from "./cancel-queues"; import { CancelQueues } from "./cancel-queues";
import { RefreshToken } from "./refresh-token"; import { RefreshToken } from "./refresh-token";
import { ShowDeployment } from "./show-deployment"; import { ShowDeployment } from "./show-deployment";
import { Badge } from "@/components/ui/badge";
interface Props { interface Props {
id: string; id: string;

View File

@@ -1,3 +1,5 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -6,8 +8,6 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { AlertBlock } from "@/components/shared/alert-block";
import { Copy, HelpCircle, Server } from "lucide-react"; import { Copy, HelpCircle, Server } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";

View File

@@ -1,4 +1,5 @@
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Card, Card,
@@ -7,6 +8,12 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { import {
CheckCircle2, CheckCircle2,
@@ -21,17 +28,10 @@ import {
XCircle, XCircle,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { toast } from "sonner";
import { AddDomain } from "./handle-domain";
import { useState } from "react"; import { useState } from "react";
import { import { toast } from "sonner";
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { DnsHelperModal } from "./dns-helper-modal"; import { DnsHelperModal } from "./dns-helper-modal";
import { Badge } from "@/components/ui/badge"; import { AddDomain } from "./handle-domain";
export type ValidationState = { export type ValidationState = {
isLoading: boolean; isLoading: boolean;

View File

@@ -17,13 +17,13 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { KeyRoundIcon, LockIcon, X } from "lucide-react"; import { KeyRoundIcon, LockIcon, X } from "lucide-react";

View File

@@ -24,9 +24,9 @@ import {
} from "lucide-react"; } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs"; import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
import { AddPreviewDomain } from "./add-preview-domain"; import { AddPreviewDomain } from "./add-preview-domain";
import { ShowPreviewSettings } from "./show-preview-settings"; import { ShowPreviewSettings } from "./show-preview-settings";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
interface Props { interface Props {
applicationId: string; applicationId: string;

View File

@@ -1,40 +1,6 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
FormDescription,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import {
Info,
PlusCircle,
PenBoxIcon,
RefreshCw,
DatabaseZap,
} from "lucide-react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Switch } from "@/components/ui/switch";
import { useEffect, useState } from "react";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -42,10 +8,44 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { toast } from "sonner"; import {
import { AlertBlock } from "@/components/shared/alert-block"; Form,
import { CodeEditor } from "@/components/shared/code-editor"; FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import {
DatabaseZap,
Info,
PenBoxIcon,
PlusCircle,
RefreshCw,
} from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import type { CacheType } from "../domains/handle-domain"; import type { CacheType } from "../domains/handle-domain";
export const commonCronExpressions = [ export const commonCronExpressions = [

View File

@@ -1,14 +1,6 @@
import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { HandleSchedules } from "./handle-schedules";
import {
Clock,
Play,
Terminal,
Trash2,
ClipboardList,
Loader2,
} from "lucide-react";
import { import {
Card, Card,
CardContent, CardContent,
@@ -16,16 +8,24 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { toast } from "sonner";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { DialogAction } from "@/components/shared/dialog-action"; import { api } from "@/utils/api";
import {
ClipboardList,
Clock,
Loader2,
Play,
Terminal,
Trash2,
} from "lucide-react";
import { toast } from "sonner";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal"; import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
import { HandleSchedules } from "./handle-schedules";
interface Props { interface Props {
id: string; id: string;

View File

@@ -71,8 +71,8 @@ export const IsolatedDeployment = ({ composeId }: Props) => {
isolatedDeployment: formData?.isolatedDeployment || false, isolatedDeployment: formData?.isolatedDeployment || false,
}) })
.then(async (_data) => { .then(async (_data) => {
randomizeCompose(); await randomizeCompose();
refetch(); await refetch();
toast.success("Compose updated"); toast.success("Compose updated");
}) })
.catch(() => { .catch(() => {
@@ -84,15 +84,10 @@ export const IsolatedDeployment = ({ composeId }: Props) => {
await mutateAsync({ await mutateAsync({
composeId, composeId,
suffix: data?.appName || "", suffix: data?.appName || "",
}) }).then(async (data) => {
.then(async (data) => { await utils.project.all.invalidate();
await utils.project.all.invalidate(); setCompose(data);
setCompose(data); });
toast.success("Compose Isolated");
})
.catch(() => {
toast.error("Error isolating the compose");
});
}; };
return ( return (

View File

@@ -77,8 +77,8 @@ export const RandomizeCompose = ({ composeId }: Props) => {
randomize: formData?.randomize || false, randomize: formData?.randomize || false,
}) })
.then(async (_data) => { .then(async (_data) => {
randomizeCompose(); await randomizeCompose();
refetch(); await refetch();
toast.success("Compose updated"); toast.success("Compose updated");
}) })
.catch(() => { .catch(() => {
@@ -90,15 +90,10 @@ export const RandomizeCompose = ({ composeId }: Props) => {
await mutateAsync({ await mutateAsync({
composeId, composeId,
suffix, suffix,
}) }).then(async (data) => {
.then(async (data) => { await utils.project.all.invalidate();
await utils.project.all.invalidate(); setCompose(data);
setCompose(data); });
toast.success("Compose randomized");
})
.catch(() => {
toast.error("Error randomizing the compose");
});
}; };
return ( return (

View File

@@ -39,6 +39,12 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@@ -48,9 +54,9 @@ import {
CheckIcon, CheckIcon,
ChevronsUpDown, ChevronsUpDown,
Copy, Copy,
RotateCcw,
RefreshCw,
DatabaseZap, DatabaseZap,
RefreshCw,
RotateCcw,
} from "lucide-react"; } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@@ -58,12 +64,6 @@ import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import type { ServiceType } from "../../application/advanced/show-resources"; import type { ServiceType } from "../../application/advanced/show-resources";
import { type LogLine, parseLogs } from "../../docker/logs/utils"; import { type LogLine, parseLogs } from "../../docker/logs/utils";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
type DatabaseType = type DatabaseType =
| Exclude<ServiceType, "application" | "redis"> | Exclude<ServiceType, "application" | "redis">

View File

@@ -1,3 +1,10 @@
import {
MariadbIcon,
MongodbIcon,
MysqlIcon,
PostgresqlIcon,
} from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -13,6 +20,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { import {
ClipboardList, ClipboardList,
@@ -25,17 +33,9 @@ import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import type { ServiceType } from "../../application/advanced/show-resources"; import type { ServiceType } from "../../application/advanced/show-resources";
import { RestoreBackup } from "./restore-backup";
import { HandleBackup } from "./handle-backup";
import { cn } from "@/lib/utils";
import {
MariadbIcon,
MongodbIcon,
MysqlIcon,
PostgresqlIcon,
} from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { ShowDeploymentsModal } from "../../application/deployments/show-deployments-modal"; import { ShowDeploymentsModal } from "../../application/deployments/show-deployments-modal";
import { HandleBackup } from "./handle-backup";
import { RestoreBackup } from "./restore-backup";
interface Props { interface Props {
id: string; id: string;

View File

@@ -1,24 +1,9 @@
"use client"; "use client";
import { authClient } from "@/lib/auth-client"; import { Logo } from "@/components/shared/logo";
import { useEffect, useState } from "react"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
CheckIcon,
ChevronsUpDown,
Settings2,
UserIcon,
XIcon,
Shield,
Calendar,
Key,
Copy,
Fingerprint,
Building2,
CreditCard,
Server,
} from "lucide-react";
import { toast } from "sonner";
import { import {
Command, Command,
CommandEmpty, CommandEmpty,
@@ -32,19 +17,34 @@ import {
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover"; } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { Logo } from "@/components/shared/logo";
import { Badge } from "@/components/ui/badge";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipTrigger,
TooltipProvider, TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { authClient } from "@/lib/auth-client";
import { format } from "date-fns"; import { cn } from "@/lib/utils";
import copy from "copy-to-clipboard";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
import { format } from "date-fns";
import {
Building2,
Calendar,
CheckIcon,
ChevronsUpDown,
Copy,
CreditCard,
Fingerprint,
Key,
Server,
Settings2,
Shield,
UserIcon,
XIcon,
} from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
type User = typeof authClient.$Infer.Session.user; type User = typeof authClient.$Infer.Session.user;

View File

@@ -10,6 +10,7 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Copy, Loader2 } from "lucide-react"; import { Copy, Loader2 } from "lucide-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@@ -48,6 +49,7 @@ export const DuplicateProject = ({
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [name, setName] = useState(""); const [name, setName] = useState("");
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const [duplicateType, setDuplicateType] = useState("new-project"); // "new-project" or "same-project"
const utils = api.useUtils(); const utils = api.useUtils();
const router = useRouter(); const router = useRouter();
@@ -59,9 +61,15 @@ export const DuplicateProject = ({
api.project.duplicate.useMutation({ api.project.duplicate.useMutation({
onSuccess: async (newProject) => { onSuccess: async (newProject) => {
await utils.project.all.invalidate(); await utils.project.all.invalidate();
toast.success("Project duplicated successfully"); toast.success(
duplicateType === "new-project"
? "Project duplicated successfully"
: "Services duplicated successfully",
);
setOpen(false); setOpen(false);
router.push(`/dashboard/project/${newProject.projectId}`); if (duplicateType === "new-project") {
router.push(`/dashboard/project/${newProject.projectId}`);
}
}, },
onError: (error) => { onError: (error) => {
toast.error(error.message); toast.error(error.message);
@@ -69,7 +77,7 @@ export const DuplicateProject = ({
}); });
const handleDuplicate = async () => { const handleDuplicate = async () => {
if (!name) { if (duplicateType === "new-project" && !name) {
toast.error("Project name is required"); toast.error("Project name is required");
return; return;
} }
@@ -83,6 +91,7 @@ export const DuplicateProject = ({
id: service.id, id: service.id,
type: service.type, type: service.type,
})), })),
duplicateInSameProject: duplicateType === "same-project",
}); });
}; };
@@ -95,6 +104,7 @@ export const DuplicateProject = ({
// Reset form when closing // Reset form when closing
setName(""); setName("");
setDescription(""); setDescription("");
setDuplicateType("new-project");
} }
}} }}
> >
@@ -106,32 +116,54 @@ export const DuplicateProject = ({
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Duplicate Project</DialogTitle> <DialogTitle>Duplicate Services</DialogTitle>
<DialogDescription> <DialogDescription>
Create a new project with the selected services Choose where to duplicate the selected services
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="name">Name</Label> <Label>Duplicate to</Label>
<Input <RadioGroup
id="name" value={duplicateType}
value={name} onValueChange={setDuplicateType}
onChange={(e) => setName(e.target.value)} className="grid gap-2"
placeholder="New project name" >
/> <div className="flex items-center space-x-2">
<RadioGroupItem value="new-project" id="new-project" />
<Label htmlFor="new-project">New project</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="same-project" id="same-project" />
<Label htmlFor="same-project">Same project</Label>
</div>
</RadioGroup>
</div> </div>
<div className="grid gap-2"> {duplicateType === "new-project" && (
<Label htmlFor="description">Description</Label> <>
<Input <div className="grid gap-2">
id="description" <Label htmlFor="name">Name</Label>
value={description} <Input
onChange={(e) => setDescription(e.target.value)} id="name"
placeholder="Project description (optional)" value={name}
/> onChange={(e) => setName(e.target.value)}
</div> placeholder="New project name"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="description">Description</Label>
<Input
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Project description (optional)"
/>
</div>
</>
)}
<div className="grid gap-2"> <div className="grid gap-2">
<Label>Selected services to duplicate</Label> <Label>Selected services to duplicate</Label>
@@ -159,10 +191,14 @@ export const DuplicateProject = ({
{isLoading ? ( {isLoading ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
Duplicating... {duplicateType === "new-project"
? "Duplicating project..."
: "Duplicating services..."}
</> </>
) : duplicateType === "new-project" ? (
"Duplicate project"
) : ( ) : (
"Duplicate" "Duplicate services"
)} )}
</Button> </Button>
</DialogFooter> </DialogFooter>

View File

@@ -18,6 +18,7 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Switch } from "@/components/ui/switch";
import { generateSHA256Hash } from "@/lib/utils"; import { generateSHA256Hash } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@@ -29,7 +30,6 @@ import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { Disable2FA } from "./disable-2fa"; import { Disable2FA } from "./disable-2fa";
import { Enable2FA } from "./enable-2fa"; import { Enable2FA } from "./enable-2fa";
import { Switch } from "@/components/ui/switch";
const profileSchema = z.object({ const profileSchema = z.object({
email: z.string(), email: z.string(),

View File

@@ -156,6 +156,67 @@ export const HandleServers = ({ serverId }: Props) => {
remotely. remotely.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div>
<p className="text-primary text-sm font-medium">
You will need to purchase or rent a Virtual Private Server (VPS) to
proceed, we recommend to use one of these providers since has been
heavily tested.
</p>
<ul className="list-inside list-disc pl-4 text-sm text-muted-foreground mt-4">
<li>
<a
href="https://www.hostinger.com/vps-hosting?REFERRALCODE=1SIUMAURICI97"
className="text-link underline"
>
Hostinger - Get 20% Discount
</a>
</li>
<li>
<a
href=" https://app.americancloud.com/register?ref=dokploy"
className="text-link underline"
>
American Cloud - Get $20 Credits
</a>
</li>
<li>
<a
href="https://m.do.co/c/db24efd43f35"
className="text-link underline"
>
DigitalOcean - Get $200 Credits
</a>
</li>
<li>
<a
href="https://hetzner.cloud/?ref=vou4fhxJ1W2D"
className="text-link underline"
>
Hetzner - Get 20 Credits
</a>
</li>
<li>
<a
href="https://www.vultr.com/?ref=9679828"
className="text-link underline"
>
Vultr
</a>
</li>
<li>
<a
href="https://www.linode.com/es/pricing/#compute-shared"
className="text-link underline"
>
Linode
</a>
</li>
</ul>
<AlertBlock className="mt-4 px-4">
You are free to use whatever provider, but we recommend to use one
of the above, to avoid issues.
</AlertBlock>
</div>
{!canCreateMoreServers && ( {!canCreateMoreServers && (
<AlertBlock type="warning"> <AlertBlock type="warning">
You cannot create more servers,{" "} You cannot create more servers,{" "}

View File

@@ -1,7 +1,7 @@
import { useState } from "react"; import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules"; import { useState } from "react";
interface Props { interface Props {
serverId: string; serverId: string;

View File

@@ -40,10 +40,10 @@ import { HandleServers } from "./handle-servers";
import { SetupServer } from "./setup-server"; import { SetupServer } from "./setup-server";
import { ShowDockerContainersModal } from "./show-docker-containers-modal"; import { ShowDockerContainersModal } from "./show-docker-containers-modal";
import { ShowMonitoringModal } from "./show-monitoring-modal"; import { ShowMonitoringModal } from "./show-monitoring-modal";
import { ShowSchedulesModal } from "./show-schedules-modal";
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal"; import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal"; import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription"; import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
import { ShowSchedulesModal } from "./show-schedules-modal";
export const ShowServers = () => { export const ShowServers = () => {
const { t } = useTranslation("settings"); const { t } = useTranslation("settings");

View File

@@ -177,6 +177,14 @@ export const WelcomeSuscription = () => {
Hostinger - Get 20% Discount Hostinger - Get 20% Discount
</a> </a>
</li> </li>
<li>
<a
href=" https://app.americancloud.com/register?ref=dokploy"
className="text-link underline"
>
American Cloud - Get $20 Credits
</a>
</li>
<li> <li>
<a <a
href="https://m.do.co/c/db24efd43f35" href="https://m.do.co/c/db24efd43f35"

View File

@@ -1,6 +1,7 @@
import Page from "./side";
import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar";
import Page from "./side";
import { ChatwootWidget } from "../shared/ChatwootWidget";
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
@@ -9,10 +10,15 @@ interface Props {
export const DashboardLayout = ({ children }: Props) => { export const DashboardLayout = ({ children }: Props) => {
const { data: haveRootAccess } = api.user.haveRootAccess.useQuery(); const { data: haveRootAccess } = api.user.haveRootAccess.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
return ( return (
<> <>
<Page>{children}</Page> <Page>{children}</Page>
{isCloud === true && (
<ChatwootWidget websiteToken="USCpQRKzHvFMssf3p6Eacae5" />
)}
{haveRootAccess === true && <ImpersonationBar />} {haveRootAccess === true && <ImpersonationBar />}
</> </>
); );

View File

@@ -1,9 +0,0 @@
import Page from "./side";
interface Props {
children: React.ReactNode;
}
export const ProjectLayout = ({ children }: Props) => {
return <Page>{children}</Page>;
};

View File

@@ -0,0 +1,69 @@
import Script from "next/script";
import { useEffect } from "react";
interface ChatwootWidgetProps {
websiteToken: string;
baseUrl?: string;
settings?: {
position?: "left" | "right";
type?: "standard" | "expanded_bubble";
launcherTitle?: string;
darkMode?: boolean;
hideMessageBubble?: boolean;
placement?: "right" | "left";
showPopoutButton?: boolean;
widgetStyle?: "standard" | "bubble";
};
user?: {
identifier: string;
name?: string;
email?: string;
phoneNumber?: string;
avatarUrl?: string;
customAttributes?: Record<string, any>;
identifierHash?: string;
};
}
export const ChatwootWidget = ({
websiteToken,
baseUrl = "https://app.chatwoot.com",
settings = {
position: "right",
type: "standard",
launcherTitle: "Chat with us",
},
user,
}: ChatwootWidgetProps) => {
useEffect(() => {
// Configurar los settings de Chatwoot
window.chatwootSettings = {
position: "right",
};
(window as any).chatwootSDKReady = () => {
window.chatwootSDK?.run({ websiteToken, baseUrl });
const trySetUser = () => {
if (window.$chatwoot && user) {
window.$chatwoot.setUser(user.identifier, {
email: user.email,
name: user.name,
avatar_url: user.avatarUrl,
phone_number: user.phoneNumber,
});
}
};
trySetUser();
};
}, [websiteToken, baseUrl, user, settings]);
return (
<Script
src={`${baseUrl}/packs/js/sdk.js`}
strategy="lazyOnload"
onLoad={() => (window as any).chatwootSDKReady?.()}
/>
);
};

View File

@@ -0,0 +1 @@
ALTER TABLE "application" ADD COLUMN "isStaticSpa" boolean;

File diff suppressed because it is too large Load Diff

View File

@@ -645,6 +645,13 @@
"when": 1746518402168, "when": 1746518402168,
"tag": "0091_spotty_kulan_gath", "tag": "0091_spotty_kulan_gath",
"breakpoints": true "breakpoints": true
},
{
"idx": 92,
"version": "7",
"when": 1747713229160,
"tag": "0092_stiff_the_watchers",
"breakpoints": true
} }
] ]
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.22.4", "version": "v0.22.7",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
@@ -36,6 +36,8 @@
"test": "vitest --config __test__/vitest.config.ts" "test": "vitest --config __test__/vitest.config.ts"
}, },
"dependencies": { "dependencies": {
"pino": "9.4.0",
"pino-pretty": "11.2.2",
"@ai-sdk/anthropic": "^1.0.6", "@ai-sdk/anthropic": "^1.0.6",
"@ai-sdk/azure": "^1.0.15", "@ai-sdk/azure": "^1.0.15",
"@ai-sdk/cohere": "^1.0.6", "@ai-sdk/cohere": "^1.0.6",
@@ -186,7 +188,7 @@
}, },
"packageManager": "pnpm@9.5.0", "packageManager": "pnpm@9.5.0",
"engines": { "engines": {
"node": "^20.9.0", "node": "^20.16.0",
"pnpm": ">=9.5.0" "pnpm": ">=9.5.0"
}, },
"lint-staged": { "lint-staged": {

View File

@@ -17,7 +17,6 @@ const inter = Inter({ subsets: ["latin"] });
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & { export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode; getLayout?: (page: ReactElement) => ReactNode;
// session: Session | null;
theme?: string; theme?: string;
}; };
@@ -33,11 +32,13 @@ const MyApp = ({
return ( return (
<> <>
<style jsx global>{` <style jsx global>
:root { {`
--font-inter: ${inter.style.fontFamily}; :root {
} --font-inter: ${inter.style.fontFamily};
`}</style> }
`}
</style>
<Head> <Head>
<title>Dokploy</title> <title>Dokploy</title>
</Head> </Head>

View File

@@ -10,7 +10,7 @@ import {
PostgresqlIcon, PostgresqlIcon,
RedisIcon, RedisIcon,
} from "@/components/icons/data-tools-icons"; } from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DateTooltip } from "@/components/shared/date-tooltip"; import { DateTooltip } from "@/components/shared/date-tooltip";
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
@@ -18,6 +18,7 @@ import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { AddAiAssistant } from "@/components/dashboard/project/add-ai-assistant"; import { AddAiAssistant } from "@/components/dashboard/project/add-ai-assistant";
import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
import { import {
Card, Card,
CardContent, CardContent,
@@ -93,7 +94,6 @@ import { useRouter } from "next/router";
import { type ReactElement, useEffect, useMemo, useState } from "react"; import { type ReactElement, useEffect, useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import superjson from "superjson"; import superjson from "superjson";
import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
export type Services = { export type Services = {
appName: string; appName: string;
@@ -1064,7 +1064,7 @@ const Project = (
export default Project; export default Project;
Project.getLayout = (page: ReactElement) => { Project.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>; return <DashboardLayout>{page}</DashboardLayout>;
}; };
export async function getServerSideProps( export async function getServerSideProps(

View File

@@ -17,7 +17,7 @@ import { UpdateApplication } from "@/components/dashboard/application/update-app
import { DeleteService } from "@/components/dashboard/compose/delete-service"; import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring"; import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring"; import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ProjectLayout } from "@/components/layouts/project-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -363,7 +363,7 @@ const Service = (
export default Service; export default Service;
Service.getLayout = (page: ReactElement) => { Service.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>; return <DashboardLayout>{page}</DashboardLayout>;
}; };
export async function getServerSideProps( export async function getServerSideProps(

View File

@@ -13,7 +13,7 @@ import { UpdateCompose } from "@/components/dashboard/compose/update-compose";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups"; import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { ComposeFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-compose-monitoring"; import { ComposeFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-compose-monitoring";
import { ComposePaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring"; import { ComposePaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring";
import { ProjectLayout } from "@/components/layouts/project-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -366,7 +366,7 @@ const Service = (
export default Service; export default Service;
Service.getLayout = (page: ReactElement) => { Service.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>; return <DashboardLayout>{page}</DashboardLayout>;
}; };
export async function getServerSideProps( export async function getServerSideProps(

View File

@@ -10,7 +10,7 @@ import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring"; import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings"; import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { MariadbIcon } from "@/components/icons/data-tools-icons"; import { MariadbIcon } from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -294,7 +294,7 @@ const Mariadb = (
export default Mariadb; export default Mariadb;
Mariadb.getLayout = (page: ReactElement) => { Mariadb.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>; return <DashboardLayout>{page}</DashboardLayout>;
}; };
export async function getServerSideProps( export async function getServerSideProps(

View File

@@ -10,7 +10,7 @@ import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring"; import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings"; import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { MongodbIcon } from "@/components/icons/data-tools-icons"; import { MongodbIcon } from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -296,7 +296,7 @@ const Mongo = (
export default Mongo; export default Mongo;
Mongo.getLayout = (page: ReactElement) => { Mongo.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>; return <DashboardLayout>{page}</DashboardLayout>;
}; };
export async function getServerSideProps( export async function getServerSideProps(

View File

@@ -10,7 +10,7 @@ import { ShowInternalMysqlCredentials } from "@/components/dashboard/mysql/gener
import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql"; import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings"; import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { MysqlIcon } from "@/components/icons/data-tools-icons"; import { MysqlIcon } from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -280,7 +280,7 @@ const MySql = (
export default MySql; export default MySql;
MySql.getLayout = (page: ReactElement) => { MySql.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>; return <DashboardLayout>{page}</DashboardLayout>;
}; };
export async function getServerSideProps( export async function getServerSideProps(

View File

@@ -10,7 +10,7 @@ import { ShowInternalPostgresCredentials } from "@/components/dashboard/postgres
import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres"; import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings"; import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { PostgresqlIcon } from "@/components/icons/data-tools-icons"; import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -278,7 +278,7 @@ const Postgresql = (
export default Postgresql; export default Postgresql;
Postgresql.getLayout = (page: ReactElement) => { Postgresql.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>; return <DashboardLayout>{page}</DashboardLayout>;
}; };
export async function getServerSideProps( export async function getServerSideProps(

View File

@@ -9,7 +9,7 @@ import { ShowInternalRedisCredentials } from "@/components/dashboard/redis/gener
import { UpdateRedis } from "@/components/dashboard/redis/update-redis"; import { UpdateRedis } from "@/components/dashboard/redis/update-redis";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings"; import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { RedisIcon } from "@/components/icons/data-tools-icons"; import { RedisIcon } from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -285,7 +285,7 @@ const Redis = (
export default Redis; export default Redis;
Redis.getLayout = (page: ReactElement) => { Redis.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>; return <DashboardLayout>{page}</DashboardLayout>;
}; };
export async function getServerSideProps( export async function getServerSideProps(

View File

@@ -1,11 +1,11 @@
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import type { ReactElement } from "react";
import type { GetServerSidePropsContext } from "next";
import { validateRequest } from "@dokploy/server/lib/auth";
import { IS_CLOUD } from "@dokploy/server/constants";
import { api } from "@/utils/api";
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules"; import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { api } from "@/utils/api";
import { IS_CLOUD } from "@dokploy/server/constants";
import { validateRequest } from "@dokploy/server/lib/auth";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
function SchedulesPage() { function SchedulesPage() {
const { data: user } = api.user.get.useQuery(); const { data: user } = api.user.get.useQuery();
return ( return (

View File

@@ -1,16 +1,16 @@
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { WebDomain } from "@/components/dashboard/settings/web-domain"; import { WebDomain } from "@/components/dashboard/settings/web-domain";
import { WebServer } from "@/components/dashboard/settings/web-server"; import { WebServer } from "@/components/dashboard/settings/web-server";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { Card } from "@/components/ui/card";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { getLocale, serverSideTranslations } from "@/utils/i18n"; import { getLocale, serverSideTranslations } from "@/utils/i18n";
import { IS_CLOUD, validateRequest } from "@dokploy/server"; import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react"; import type { ReactElement } from "react";
import superjson from "superjson"; import superjson from "superjson";
import { api } from "@/utils/api";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { Card } from "@/components/ui/card";
const Page = () => { const Page = () => {
const { data: user } = api.user.get.useQuery(); const { data: user } = api.user.get.useQuery();
return ( return (

View File

@@ -28,6 +28,7 @@ import { projectRouter } from "./routers/project";
import { redirectsRouter } from "./routers/redirects"; import { redirectsRouter } from "./routers/redirects";
import { redisRouter } from "./routers/redis"; import { redisRouter } from "./routers/redis";
import { registryRouter } from "./routers/registry"; import { registryRouter } from "./routers/registry";
import { scheduleRouter } from "./routers/schedule";
import { securityRouter } from "./routers/security"; import { securityRouter } from "./routers/security";
import { serverRouter } from "./routers/server"; import { serverRouter } from "./routers/server";
import { settingsRouter } from "./routers/settings"; import { settingsRouter } from "./routers/settings";
@@ -35,7 +36,6 @@ import { sshRouter } from "./routers/ssh-key";
import { stripeRouter } from "./routers/stripe"; import { stripeRouter } from "./routers/stripe";
import { swarmRouter } from "./routers/swarm"; import { swarmRouter } from "./routers/swarm";
import { userRouter } from "./routers/user"; import { userRouter } from "./routers/user";
import { scheduleRouter } from "./routers/schedule";
/** /**
* This is the primary router for your server. * This is the primary router for your server.
* *

View File

@@ -330,6 +330,7 @@ export const applicationRouter = createTRPCRouter({
dockerContextPath: input.dockerContextPath, dockerContextPath: input.dockerContextPath,
dockerBuildStage: input.dockerBuildStage, dockerBuildStage: input.dockerBuildStage,
herokuVersion: input.herokuVersion, herokuVersion: input.herokuVersion,
isStaticSpa: input.isStaticSpa,
}); });
return true; return true;

View File

@@ -51,9 +51,9 @@ import { processTemplate } from "@dokploy/server/templates/processors";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { dump } from "js-yaml"; import { dump } from "js-yaml";
import { parse } from "toml";
import _ from "lodash"; import _ from "lodash";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { parse } from "toml";
import { z } from "zod"; import { z } from "zod";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
@@ -439,7 +439,15 @@ export const composeRouter = createTRPCRouter({
} }
const projectName = slugify(`${project.name} ${input.id}`); const projectName = slugify(`${project.name} ${input.id}`);
const generate = processTemplate(template.config, { const appName = `${projectName}-${generatePassword(6)}`;
const config = {
...template.config,
variables: {
APP_NAME: appName,
...template.config.variables,
},
};
const generate = processTemplate(config, {
serverIp: serverIp, serverIp: serverIp,
projectName: projectName, projectName: projectName,
}); });
@@ -451,7 +459,7 @@ export const composeRouter = createTRPCRouter({
serverId: input.serverId, serverId: input.serverId,
name: input.id, name: input.id,
sourceType: "raw", sourceType: "raw",
appName: `${projectName}-${generatePassword(6)}`, appName: appName,
isolatedDeployment: true, isolatedDeployment: true,
}); });
@@ -605,7 +613,15 @@ export const composeRouter = createTRPCRouter({
}); });
} }
const processedTemplate = processTemplate(config, { const configModified = {
...config,
variables: {
APP_NAME: compose.appName,
...config.variables,
},
};
const processedTemplate = processTemplate(configModified, {
serverIp: serverIp, serverIp: serverIp,
projectName: compose.appName, projectName: compose.appName,
}); });
@@ -675,7 +691,15 @@ export const composeRouter = createTRPCRouter({
}); });
} }
const processedTemplate = processTemplate(config, { const configModified = {
...config,
variables: {
APP_NAME: compose.appName,
...config.variables,
},
};
const processedTemplate = processTemplate(configModified, {
serverIp: serverIp, serverIp: serverIp,
projectName: compose.appName, projectName: compose.appName,
}); });

View File

@@ -1,3 +1,4 @@
import { db } from "@/server/db";
import { import {
apiFindAllByApplication, apiFindAllByApplication,
apiFindAllByCompose, apiFindAllByCompose,
@@ -16,7 +17,6 @@ import {
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm"; import { desc, eq } from "drizzle-orm";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
import { db } from "@/server/db";
export const deploymentRouter = createTRPCRouter({ export const deploymentRouter = createTRPCRouter({
all: protectedProcedure all: protectedProcedure

View File

@@ -21,32 +21,32 @@ import {
addNewProject, addNewProject,
checkProjectAccess, checkProjectAccess,
createApplication, createApplication,
createBackup,
createCompose, createCompose,
createDomain,
createMariadb, createMariadb,
createMongo, createMongo,
createMount,
createMysql, createMysql,
createPort,
createPostgres, createPostgres,
createPreviewDeployment,
createProject, createProject,
createRedirect,
createRedis, createRedis,
createSecurity,
deleteProject, deleteProject,
findApplicationById, findApplicationById,
findComposeById, findComposeById,
findMongoById, findMariadbById,
findMemberById, findMemberById,
findRedisById, findMongoById,
findMySqlById,
findPostgresById,
findProjectById, findProjectById,
findRedisById,
findUserById, findUserById,
updateProjectById, updateProjectById,
findPostgresById,
findMariadbById,
findMySqlById,
createDomain,
createPort,
createMount,
createRedirect,
createPreviewDeployment,
createBackup,
createSecurity,
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { and, desc, eq, sql } from "drizzle-orm"; import { and, desc, eq, sql } from "drizzle-orm";
@@ -309,6 +309,7 @@ export const projectRouter = createTRPCRouter({
}), }),
) )
.optional(), .optional(),
duplicateInSameProject: z.boolean().default(false),
}), }),
) )
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
@@ -331,15 +332,17 @@ export const projectRouter = createTRPCRouter({
}); });
} }
// Create new project // Create new project or use existing one
const newProject = await createProject( const targetProject = input.duplicateInSameProject
{ ? sourceProject
name: input.name, : await createProject(
description: input.description, {
env: sourceProject.env, name: input.name,
}, description: input.description,
ctx.session.activeOrganizationId, env: sourceProject.env,
); },
ctx.session.activeOrganizationId,
);
if (input.includeServices) { if (input.includeServices) {
const servicesToDuplicate = input.selectedServices || []; const servicesToDuplicate = input.selectedServices || [];
@@ -362,7 +365,10 @@ export const projectRouter = createTRPCRouter({
const newApplication = await createApplication({ const newApplication = await createApplication({
...application, ...application,
projectId: newProject.projectId, name: input.duplicateInSameProject
? `${application.name} (copy)`
: application.name,
projectId: targetProject.projectId,
}); });
for (const domain of domains) { for (const domain of domains) {
@@ -423,7 +429,10 @@ export const projectRouter = createTRPCRouter({
const newPostgres = await createPostgres({ const newPostgres = await createPostgres({
...postgres, ...postgres,
projectId: newProject.projectId, name: input.duplicateInSameProject
? `${postgres.name} (copy)`
: postgres.name,
projectId: targetProject.projectId,
}); });
for (const mount of mounts) { for (const mount of mounts) {
@@ -449,7 +458,10 @@ export const projectRouter = createTRPCRouter({
await findMariadbById(id); await findMariadbById(id);
const newMariadb = await createMariadb({ const newMariadb = await createMariadb({
...mariadb, ...mariadb,
projectId: newProject.projectId, name: input.duplicateInSameProject
? `${mariadb.name} (copy)`
: mariadb.name,
projectId: targetProject.projectId,
}); });
for (const mount of mounts) { for (const mount of mounts) {
@@ -475,7 +487,10 @@ export const projectRouter = createTRPCRouter({
await findMongoById(id); await findMongoById(id);
const newMongo = await createMongo({ const newMongo = await createMongo({
...mongo, ...mongo,
projectId: newProject.projectId, name: input.duplicateInSameProject
? `${mongo.name} (copy)`
: mongo.name,
projectId: targetProject.projectId,
}); });
for (const mount of mounts) { for (const mount of mounts) {
@@ -501,7 +516,10 @@ export const projectRouter = createTRPCRouter({
await findMySqlById(id); await findMySqlById(id);
const newMysql = await createMysql({ const newMysql = await createMysql({
...mysql, ...mysql,
projectId: newProject.projectId, name: input.duplicateInSameProject
? `${mysql.name} (copy)`
: mysql.name,
projectId: targetProject.projectId,
}); });
for (const mount of mounts) { for (const mount of mounts) {
@@ -526,7 +544,10 @@ export const projectRouter = createTRPCRouter({
const { redisId, mounts, ...redis } = await findRedisById(id); const { redisId, mounts, ...redis } = await findRedisById(id);
const newRedis = await createRedis({ const newRedis = await createRedis({
...redis, ...redis,
projectId: newProject.projectId, name: input.duplicateInSameProject
? `${redis.name} (copy)`
: redis.name,
projectId: targetProject.projectId,
}); });
for (const mount of mounts) { for (const mount of mounts) {
@@ -545,7 +566,10 @@ export const projectRouter = createTRPCRouter({
await findComposeById(id); await findComposeById(id);
const newCompose = await createCompose({ const newCompose = await createCompose({
...compose, ...compose,
projectId: newProject.projectId, name: input.duplicateInSameProject
? `${compose.name} (copy)`
: compose.name,
projectId: targetProject.projectId,
}); });
for (const mount of mounts) { for (const mount of mounts) {
@@ -572,21 +596,20 @@ export const projectRouter = createTRPCRouter({
}; };
// Duplicate selected services // Duplicate selected services
for (const service of servicesToDuplicate) { for (const service of servicesToDuplicate) {
await duplicateService(service.id, service.type); await duplicateService(service.id, service.type);
} }
} }
if (ctx.user.role === "member") { if (!input.duplicateInSameProject && ctx.user.role === "member") {
await addNewProject( await addNewProject(
ctx.user.id, ctx.user.id,
newProject.projectId, targetProject.projectId,
ctx.session.activeOrganizationId, ctx.session.activeOrganizationId,
); );
} }
return newProject; return targetProject;
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",

View File

@@ -1,24 +1,24 @@
import { TRPCError } from "@trpc/server"; import { removeJob, schedule } from "@/server/utils/backup";
import { z } from "zod"; import { IS_CLOUD, scheduleJob } from "@dokploy/server";
import { removeScheduleJob } from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { deployments } from "@dokploy/server/db/schema/deployment";
import { import {
createScheduleSchema, createScheduleSchema,
schedules, schedules,
updateScheduleSchema, updateScheduleSchema,
} from "@dokploy/server/db/schema/schedule"; } from "@dokploy/server/db/schema/schedule";
import { desc, eq } from "drizzle-orm";
import { db } from "@dokploy/server/db";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { runCommand } from "@dokploy/server/index"; import { runCommand } from "@dokploy/server/index";
import { deployments } from "@dokploy/server/db/schema/deployment";
import { import {
createSchedule,
deleteSchedule, deleteSchedule,
findScheduleById, findScheduleById,
createSchedule,
updateSchedule, updateSchedule,
} from "@dokploy/server/services/schedule"; } from "@dokploy/server/services/schedule";
import { IS_CLOUD, scheduleJob } from "@dokploy/server"; import { TRPCError } from "@trpc/server";
import { removeJob, schedule } from "@/server/utils/backup"; import { desc, eq } from "drizzle-orm";
import { removeScheduleJob } from "@dokploy/server"; import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const scheduleRouter = createTRPCRouter({ export const scheduleRouter = createTRPCRouter({
create: protectedProcedure create: protectedProcedure
.input(createScheduleSchema) .input(createScheduleSchema)

View File

@@ -1,9 +1,9 @@
import type http from "node:http"; import type http from "node:http";
import { validateRequest } from "@dokploy/server/lib/auth";
import { applyWSSHandler } from "@trpc/server/adapters/ws"; import { applyWSSHandler } from "@trpc/server/adapters/ws";
import { WebSocketServer } from "ws"; import { WebSocketServer } from "ws";
import { appRouter } from "../api/root"; import { appRouter } from "../api/root";
import { createTRPCContext } from "../api/trpc"; import { createTRPCContext } from "../api/trpc";
import { validateRequest } from "@dokploy/server/lib/auth";
export const setupDrawerLogsWebSocketServer = ( export const setupDrawerLogsWebSocketServer = (
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>, server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,

39
apps/dokploy/types/chatwoot.d.ts vendored Normal file
View File

@@ -0,0 +1,39 @@
declare global {
interface Window {
chatwootSettings?: {
hideMessageBubble?: boolean;
showUnreadMessagesDialog?: boolean;
position?: "left" | "right";
locale?: string;
useBrowserLanguage?: boolean;
type?: "standard" | "expanded_bubble";
darkMode?: "light" | "auto";
launcherTitle?: string;
showPopoutButton?: boolean;
baseDomain?: string;
};
chatwootSDK?: {
run: (config: {
websiteToken: string;
baseUrl: string;
}) => void;
};
$chatwoot?: {
setUser: (
identifier: string,
userAttributes: Record<string, any>,
) => void;
setCustomAttributes: (attributes: Record<string, any>) => void;
reset: () => void;
toggle: (state?: "open" | "close") => void;
popoutChatWindow: () => void;
toggleBubbleVisibility: (visibility: "show" | "hide") => void;
setLocale: (locale: string) => void;
setLabel: (label: string) => void;
removeLabel: (label: string) => void;
};
chatwootSDKReady?: () => void;
}
}
export {};

View File

@@ -7,11 +7,11 @@ import {
findServerById, findServerById,
keepLatestNBackups, keepLatestNBackups,
runCommand, runCommand,
runComposeBackup,
runMariadbBackup, runMariadbBackup,
runMongoBackup, runMongoBackup,
runMySqlBackup, runMySqlBackup,
runPostgresBackup, runPostgresBackup,
runComposeBackup,
} from "@dokploy/server"; } from "@dokploy/server";
import { db } from "@dokploy/server/dist/db"; import { db } from "@dokploy/server/dist/db";
import { backups, schedules, server } from "@dokploy/server/dist/db/schema"; import { backups, schedules, server } from "@dokploy/server/dist/db/schema";

View File

@@ -32,7 +32,7 @@
}, },
"packageManager": "pnpm@9.5.0", "packageManager": "pnpm@9.5.0",
"engines": { "engines": {
"node": "^20.9.0", "node": "^20.16.0",
"pnpm": ">=9.5.0" "pnpm": ">=9.5.0"
}, },
"lint-staged": { "lint-staged": {

View File

@@ -28,6 +28,8 @@
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"pino": "9.4.0",
"pino-pretty": "11.2.2",
"micromatch": "4.0.8", "micromatch": "4.0.8",
"@ai-sdk/anthropic": "^1.0.6", "@ai-sdk/anthropic": "^1.0.6",
"@ai-sdk/azure": "^1.0.15", "@ai-sdk/azure": "^1.0.15",

View File

@@ -206,6 +206,7 @@ export const applications = pgTable("application", {
buildType: buildType("buildType").notNull().default("nixpacks"), buildType: buildType("buildType").notNull().default("nixpacks"),
herokuVersion: text("herokuVersion").default("24"), herokuVersion: text("herokuVersion").default("24"),
publishDirectory: text("publishDirectory"), publishDirectory: text("publishDirectory"),
isStaticSpa: boolean("isStaticSpa"),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
.$defaultFn(() => new Date().toISOString()), .$defaultFn(() => new Date().toISOString()),
@@ -409,6 +410,7 @@ const createSchema = createInsertSchema(applications, {
]), ]),
herokuVersion: z.string().optional(), herokuVersion: z.string().optional(),
publishDirectory: z.string().optional(), publishDirectory: z.string().optional(),
isStaticSpa: z.boolean().optional(),
owner: z.string(), owner: z.string(),
healthCheckSwarm: HealthCheckSwarmSchema.nullable(), healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
restartPolicySwarm: RestartPolicySwarmSchema.nullable(), restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
@@ -461,7 +463,7 @@ export const apiSaveBuildType = createSchema
herokuVersion: true, herokuVersion: true,
}) })
.required() .required()
.merge(createSchema.pick({ publishDirectory: true })); .merge(createSchema.pick({ publishDirectory: true, isStaticSpa: true }));
export const apiSaveGithubProvider = createSchema export const apiSaveGithubProvider = createSchema
.pick({ .pick({

View File

@@ -11,15 +11,15 @@ import {
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { generateAppName } from ".";
import { compose } from "./compose";
import { deployments } from "./deployment";
import { destinations } from "./destination"; import { destinations } from "./destination";
import { mariadb } from "./mariadb"; import { mariadb } from "./mariadb";
import { mongo } from "./mongo"; import { mongo } from "./mongo";
import { mysql } from "./mysql"; import { mysql } from "./mysql";
import { postgres } from "./postgres"; import { postgres } from "./postgres";
import { users_temp } from "./user"; import { users_temp } from "./user";
import { compose } from "./compose";
import { deployments } from "./deployment";
import { generateAppName } from ".";
export const databaseType = pgEnum("databaseType", [ export const databaseType = pgEnum("databaseType", [
"postgres", "postgres",
"mariadb", "mariadb",

View File

@@ -3,6 +3,7 @@ import { boolean, integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { backups } from "./backups";
import { bitbucket } from "./bitbucket"; import { bitbucket } from "./bitbucket";
import { deployments } from "./deployment"; import { deployments } from "./deployment";
import { domains } from "./domain"; import { domains } from "./domain";
@@ -15,7 +16,6 @@ import { server } from "./server";
import { applicationStatus, triggerType } from "./shared"; import { applicationStatus, triggerType } from "./shared";
import { sshKeys } from "./ssh-key"; import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils"; import { generateAppName } from "./utils";
import { backups } from "./backups";
import { schedules } from "./schedule"; import { schedules } from "./schedule";
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [ export const sourceTypeCompose = pgEnum("sourceTypeCompose", [

View File

@@ -10,11 +10,11 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { applications } from "./application"; import { applications } from "./application";
import { backups } from "./backups";
import { compose } from "./compose"; import { compose } from "./compose";
import { previewDeployments } from "./preview-deployments"; import { previewDeployments } from "./preview-deployments";
import { server } from "./server";
import { schedules } from "./schedule"; import { schedules } from "./schedule";
import { backups } from "./backups"; import { server } from "./server";
export const deploymentStatus = pgEnum("deploymentStatus", [ export const deploymentStatus = pgEnum("deploymentStatus", [
"running", "running",
"done", "done",

View File

@@ -4,11 +4,11 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { applications } from "./application"; import { applications } from "./application";
import { deployments } from "./deployment";
import { generateAppName } from "./utils";
import { compose } from "./compose"; import { compose } from "./compose";
import { deployments } from "./deployment";
import { server } from "./server"; import { server } from "./server";
import { users_temp } from "./user"; import { users_temp } from "./user";
import { generateAppName } from "./utils";
export const shellTypes = pgEnum("shellType", ["bash", "sh"]); export const shellTypes = pgEnum("shellType", ["bash", "sh"]);
export const scheduleType = pgEnum("scheduleType", [ export const scheduleType = pgEnum("scheduleType", [

View File

@@ -20,9 +20,9 @@ import { mongo } from "./mongo";
import { mysql } from "./mysql"; import { mysql } from "./mysql";
import { postgres } from "./postgres"; import { postgres } from "./postgres";
import { redis } from "./redis"; import { redis } from "./redis";
import { schedules } from "./schedule";
import { sshKeys } from "./ssh-key"; import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils"; import { generateAppName } from "./utils";
import { schedules } from "./schedule";
export const serverStatus = pgEnum("serverStatus", ["active", "inactive"]); export const serverStatus = pgEnum("serverStatus", ["active", "inactive"]);
export const server = pgTable("server", { export const server = pgTable("server", {

View File

@@ -11,10 +11,10 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { account, apikey, organization } from "./account"; import { account, apikey, organization } from "./account";
import { projects } from "./project";
import { certificateType } from "./shared";
import { backups } from "./backups"; import { backups } from "./backups";
import { projects } from "./project";
import { schedules } from "./schedule"; import { schedules } from "./schedule";
import { certificateType } from "./shared";
/** /**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
* database instance for multiple projects. * database instance for multiple projects.

View File

@@ -131,3 +131,5 @@ export {
export * from "./utils/schedules/utils"; export * from "./utils/schedules/utils";
export * from "./utils/schedules/index"; export * from "./utils/schedules/index";
export * from "./lib/logger";

View File

@@ -3,7 +3,7 @@ import * as bcrypt from "bcrypt";
import { betterAuth } from "better-auth"; import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { APIError } from "better-auth/api"; import { APIError } from "better-auth/api";
import { apiKey, organization, twoFactor, admin } from "better-auth/plugins"; import { admin, apiKey, organization, twoFactor } from "better-auth/plugins";
import { and, desc, eq } from "drizzle-orm"; import { and, desc, eq } from "drizzle-orm";
import { IS_CLOUD } from "../constants"; import { IS_CLOUD } from "../constants";
import { db } from "../db"; import { db } from "../db";

View File

@@ -0,0 +1,11 @@
import pino from "pino";
export const logger = pino({
transport: {
target: "pino-pretty",
options: {
colorize: true,
levelFirst: false,
},
},
});

View File

@@ -24,13 +24,13 @@ import { type Compose, findComposeById, updateCompose } from "./compose";
import { type Server, findServerById } from "./server"; import { type Server, findServerById } from "./server";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import { findBackupById } from "./backup";
import { import {
type PreviewDeployment, type PreviewDeployment,
findPreviewDeploymentById, findPreviewDeploymentById,
updatePreviewDeployment, updatePreviewDeployment,
} from "./preview-deployment"; } from "./preview-deployment";
import { findScheduleById } from "./schedule"; import { findScheduleById } from "./schedule";
import { findBackupById } from "./backup";
export type Deployment = typeof deployments.$inferSelect; export type Deployment = typeof deployments.$inferSelect;

View File

@@ -1,3 +1,5 @@
import dns from "node:dns";
import { promisify } from "node:util";
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
import { generateRandomDomain } from "@dokploy/server/templates"; import { generateRandomDomain } from "@dokploy/server/templates";
import { manageDomain } from "@dokploy/server/utils/traefik/domain"; import { manageDomain } from "@dokploy/server/utils/traefik/domain";
@@ -7,8 +9,6 @@ import { type apiCreateDomain, domains } from "../db/schema";
import { findUserById } from "./admin"; import { findUserById } from "./admin";
import { findApplicationById } from "./application"; import { findApplicationById } from "./application";
import { findServerById } from "./server"; import { findServerById } from "./server";
import dns from "node:dns";
import { promisify } from "node:util";
export type Domain = typeof domains.$inferSelect; export type Domain = typeof domains.$inferSelect;

View File

@@ -1,16 +1,16 @@
import { type Schedule, schedules } from "../db/schema/schedule"; import path from "node:path";
import { db } from "../db";
import { eq } from "drizzle-orm";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import type { z } from "zod"; import type { z } from "zod";
import { paths } from "../constants";
import { db } from "../db";
import { type Schedule, schedules } from "../db/schema/schedule";
import type { import type {
createScheduleSchema, createScheduleSchema,
updateScheduleSchema, updateScheduleSchema,
} from "../db/schema/schedule"; } from "../db/schema/schedule";
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
import { paths } from "../constants";
import path from "node:path";
import { encodeBase64 } from "../utils/docker/utils"; import { encodeBase64 } from "../utils/docker/utils";
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
export type ScheduleExtended = Awaited<ReturnType<typeof findScheduleById>>; export type ScheduleExtended = Awaited<ReturnType<typeof findScheduleById>>;

View File

@@ -356,20 +356,20 @@ const installUtilities = () => `
case "$OS_TYPE" in case "$OS_TYPE" in
arch) arch)
pacman -Sy --noconfirm --needed curl wget git jq openssl >/dev/null || true pacman -Sy --noconfirm --needed curl wget git git-lfs jq openssl >/dev/null || true
;; ;;
alpine) alpine)
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
apk update >/dev/null apk update >/dev/null
apk add curl wget git jq openssl sudo unzip tar >/dev/null apk add curl wget git git-lfs jq openssl sudo unzip tar >/dev/null
;; ;;
ubuntu | debian | raspbian) ubuntu | debian | raspbian)
DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git jq openssl >/dev/null DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git git-lfs jq openssl >/dev/null
;; ;;
centos | fedora | rhel | ol | rocky | almalinux | amzn) centos | fedora | rhel | ol | rocky | almalinux | amzn)
if [ "$OS_TYPE" = "amzn" ]; then if [ "$OS_TYPE" = "amzn" ]; then
dnf install -y wget git jq openssl >/dev/null dnf install -y wget git git-lfs jq openssl >/dev/null
else else
if ! command -v dnf >/dev/null; then if ! command -v dnf >/dev/null; then
yum install -y dnf >/dev/null yum install -y dnf >/dev/null
@@ -377,12 +377,12 @@ const installUtilities = () => `
if ! command -v curl >/dev/null; then if ! command -v curl >/dev/null; then
dnf install -y curl >/dev/null dnf install -y curl >/dev/null
fi fi
dnf install -y wget git jq openssl unzip >/dev/null dnf install -y wget git git-lfs jq openssl unzip >/dev/null
fi fi
;; ;;
sles | opensuse-leap | opensuse-tumbleweed) sles | opensuse-leap | opensuse-tumbleweed)
zypper refresh >/dev/null zypper refresh >/dev/null
zypper install -y curl wget git jq openssl >/dev/null zypper install -y curl wget git git-lfs jq openssl >/dev/null
;; ;;
*) *)
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now." echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
@@ -577,7 +577,7 @@ const installNixpacks = () => `
if command_exists nixpacks; then if command_exists nixpacks; then
echo "Nixpacks already installed ✅" echo "Nixpacks already installed ✅"
else else
export NIXPACKS_VERSION=1.35.0 export NIXPACKS_VERSION=1.39.0
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)" bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
echo "Nixpacks version $NIXPACKS_VERSION installed ✅" echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
fi fi

View File

@@ -1,4 +1,4 @@
import { randomBytes, createHmac } from "node:crypto"; import { createHmac, randomBytes } from "node:crypto";
import { existsSync } from "node:fs"; import { existsSync } from "node:fs";
import { mkdir, readFile, writeFile } from "node:fs/promises"; import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
@@ -35,7 +35,7 @@ export const generateRandomDomain = ({
projectName, projectName,
}: Schema): string => { }: Schema): string => {
const hash = randomBytes(3).toString("hex"); const hash = randomBytes(3).toString("hex");
const slugIp = serverIp.replaceAll(".", "-"); const slugIp = serverIp.replaceAll(".", "-").replaceAll(":", "-");
return `${projectName}-${hash}${slugIp === "" ? "" : `-${slugIp}`}.traefik.me`; return `${projectName}-${hash}${slugIp === "" ? "" : `-${slugIp}`}.traefik.me`;
}; };

View File

@@ -1,13 +1,13 @@
import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { BackupSchedule } from "@dokploy/server/services/backup";
import type { Compose } from "@dokploy/server/services/compose"; import type { Compose } from "@dokploy/server/services/compose";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials, normalizeS3Path, getBackupCommand } from "./utils";
import { import {
createDeploymentBackup, createDeploymentBackup,
updateDeploymentStatus, updateDeploymentStatus,
} from "@dokploy/server/services/deployment"; } from "@dokploy/server/services/deployment";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
export const runComposeBackup = async ( export const runComposeBackup = async (
compose: Compose, compose: Compose,

View File

@@ -11,10 +11,10 @@ import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup"
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials, scheduleBackup } from "./utils"; import { getS3Credentials, scheduleBackup } from "./utils";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { startLogCleanup } from "../access-log/handler";
import { member } from "@dokploy/server/db/schema"; import { member } from "@dokploy/server/db/schema";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { startLogCleanup } from "../access-log/handler";
export const initCronJobs = async () => { export const initCronJobs = async () => {
console.log("Setting up cron jobs...."); console.log("Setting up cron jobs....");

View File

@@ -1,13 +1,13 @@
import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { BackupSchedule } from "@dokploy/server/services/backup";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { Mariadb } from "@dokploy/server/services/mariadb"; import type { Mariadb } from "@dokploy/server/services/mariadb";
import { findProjectById } from "@dokploy/server/services/project"; import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils"; import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
export const runMariadbBackup = async ( export const runMariadbBackup = async (
mariadb: Mariadb, mariadb: Mariadb,

View File

@@ -1,13 +1,13 @@
import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { BackupSchedule } from "@dokploy/server/services/backup";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { Mongo } from "@dokploy/server/services/mongo"; import type { Mongo } from "@dokploy/server/services/mongo";
import { findProjectById } from "@dokploy/server/services/project"; import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils"; import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => { export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
const { projectId, name } = mongo; const { projectId, name } = mongo;

View File

@@ -1,13 +1,13 @@
import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { BackupSchedule } from "@dokploy/server/services/backup";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { MySql } from "@dokploy/server/services/mysql"; import type { MySql } from "@dokploy/server/services/mysql";
import { findProjectById } from "@dokploy/server/services/project"; import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils"; import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => { export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
const { projectId, name } = mysql; const { projectId, name } = mysql;

View File

@@ -1,13 +1,13 @@
import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { BackupSchedule } from "@dokploy/server/services/backup";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { Postgres } from "@dokploy/server/services/postgres"; import type { Postgres } from "@dokploy/server/services/postgres";
import { findProjectById } from "@dokploy/server/services/project"; import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils"; import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
export const runPostgresBackup = async ( export const runPostgresBackup = async (
postgres: Postgres, postgres: Postgres,

View File

@@ -1,13 +1,14 @@
import { logger } from "@dokploy/server/lib/logger";
import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { BackupSchedule } from "@dokploy/server/services/backup";
import type { Destination } from "@dokploy/server/services/destination"; import type { Destination } from "@dokploy/server/services/destination";
import { scheduleJob, scheduledJobs } from "node-schedule"; import { scheduleJob, scheduledJobs } from "node-schedule";
import { keepLatestNBackups } from "."; import { keepLatestNBackups } from ".";
import { runComposeBackup } from "./compose";
import { runMariadbBackup } from "./mariadb"; import { runMariadbBackup } from "./mariadb";
import { runMongoBackup } from "./mongo"; import { runMongoBackup } from "./mongo";
import { runMySqlBackup } from "./mysql"; import { runMySqlBackup } from "./mysql";
import { runPostgresBackup } from "./postgres"; import { runPostgresBackup } from "./postgres";
import { runWebServerBackup } from "./web-server"; import { runWebServerBackup } from "./web-server";
import { runComposeBackup } from "./compose";
export const scheduleBackup = (backup: BackupSchedule) => { export const scheduleBackup = (backup: BackupSchedule) => {
const { const {
@@ -222,6 +223,17 @@ export const getBackupCommand = (
) => { ) => {
const containerSearch = getContainerSearchCommand(backup); const containerSearch = getContainerSearchCommand(backup);
const backupCommand = generateBackupCommand(backup); const backupCommand = generateBackupCommand(backup);
logger.info(
{
containerSearch,
backupCommand,
rcloneCommand,
logPath,
},
`Executing backup command: ${backup.databaseType} ${backup.backupType}`,
);
return ` return `
set -eo pipefail; set -eo pipefail;
echo "[$(date)] Starting backup process..." >> ${logPath}; echo "[$(date)] Starting backup process..." >> ${logPath};

View File

@@ -1,16 +1,16 @@
import type { BackupSchedule } from "@dokploy/server/services/backup"; import { createWriteStream } from "node:fs";
import { execAsync } from "../process/execAsync";
import { getS3Credentials, normalizeS3Path } from "./utils";
import { findDestinationById } from "@dokploy/server/services/destination";
import { IS_CLOUD, paths } from "@dokploy/server/constants";
import { mkdtemp, rm } from "node:fs/promises"; import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path";
import { IS_CLOUD, paths } from "@dokploy/server/constants";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { import {
createDeploymentBackup, createDeploymentBackup,
updateDeploymentStatus, updateDeploymentStatus,
} from "@dokploy/server/services/deployment"; } from "@dokploy/server/services/deployment";
import { createWriteStream } from "node:fs"; import { findDestinationById } from "@dokploy/server/services/destination";
import { execAsync } from "../process/execAsync";
import { getS3Credentials, normalizeS3Path } from "./utils";
export const runWebServerBackup = async (backup: BackupSchedule) => { export const runWebServerBackup = async (backup: BackupSchedule) => {
if (IS_CLOUD) { if (IS_CLOUD) {
@@ -67,7 +67,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
await execAsync(cleanupCommand); await execAsync(cleanupCommand);
await execAsync( await execAsync(
`rsync -av --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`, `rsync -a --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`,
); );
writeStream.write("Copied filesystem to temp directory\n"); writeStream.write("Copied filesystem to temp directory\n");

View File

@@ -190,7 +190,8 @@ const createEnvFile = (compose: ComposeNested) => {
join(COMPOSE_PATH, appName, "code", "docker-compose.yml"); join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
const envFilePath = join(dirname(composeFilePath), ".env"); const envFilePath = join(dirname(composeFilePath), ".env");
let envContent = env || ""; let envContent = `APP_NAME=${appName}\n`;
envContent += env || "";
if (!envContent.includes("DOCKER_CONFIG")) { if (!envContent.includes("DOCKER_CONFIG")) {
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json"; envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
} }
@@ -219,7 +220,8 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => {
const envFilePath = join(dirname(composeFilePath), ".env"); const envFilePath = join(dirname(composeFilePath), ".env");
let envContent = env || ""; let envContent = `APP_NAME=${appName}\n`;
envContent += env || "";
if (!envContent.includes("DOCKER_CONFIG")) { if (!envContent.includes("DOCKER_CONFIG")) {
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json"; envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
} }

View File

@@ -7,21 +7,58 @@ import type { ApplicationNested } from ".";
import { createFile, getCreateFileCommand } from "../docker/utils"; import { createFile, getCreateFileCommand } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory"; import { getBuildAppDirectory } from "../filesystem/directory";
const nginxSpaConfig = `
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
access_log /dev/stdout;
error_log /dev/stderr;
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
}
}
`;
export const buildStatic = async ( export const buildStatic = async (
application: ApplicationNested, application: ApplicationNested,
writeStream: WriteStream, writeStream: WriteStream,
) => { ) => {
const { publishDirectory } = application; const { publishDirectory, isStaticSpa } = application;
const buildAppDirectory = getBuildAppDirectory(application); const buildAppDirectory = getBuildAppDirectory(application);
try { try {
if (isStaticSpa) {
createFile(buildAppDirectory, "nginx.conf", nginxSpaConfig);
}
createFile(
buildAppDirectory,
".dockerignore",
[".git", ".env", "Dockerfile", ".dockerignore"].join("\n"),
);
createFile( createFile(
buildAppDirectory, buildAppDirectory,
"Dockerfile", "Dockerfile",
[ [
"FROM nginx:alpine", "FROM nginx:alpine",
"WORKDIR /usr/share/nginx/html/", "WORKDIR /usr/share/nginx/html/",
isStaticSpa ? "COPY nginx.conf /etc/nginx/nginx.conf" : "",
`COPY ${publishDirectory || "."} .`, `COPY ${publishDirectory || "."} .`,
'CMD ["nginx", "-g", "daemon off;"]',
].join("\n"), ].join("\n"),
); );

View File

@@ -2,6 +2,7 @@ import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import type { Readable } from "node:stream"; import type { Readable } from "node:stream";
import { docker, paths } from "@dokploy/server/constants"; import { docker, paths } from "@dokploy/server/constants";
import type { Compose } from "@dokploy/server/services/compose";
import type { ContainerInfo, ResourceRequirements } from "dockerode"; import type { ContainerInfo, ResourceRequirements } from "dockerode";
import { parse } from "dotenv"; import { parse } from "dotenv";
import type { ApplicationNested } from "../builders"; import type { ApplicationNested } from "../builders";
@@ -13,7 +14,6 @@ import type { RedisNested } from "../databases/redis";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync"; import { spawnAsync } from "../process/spawnAsync";
import { getRemoteDocker } from "../servers/remote-docker"; import { getRemoteDocker } from "../servers/remote-docker";
import type { Compose } from "@dokploy/server/services/compose";
interface RegistryAuth { interface RegistryAuth {
username: string; username: string;

View File

@@ -246,32 +246,18 @@ export const getGitlabRepositories = async (gitlabId?: string) => {
const gitlabProvider = await findGitlabById(gitlabId); const gitlabProvider = await findGitlabById(gitlabId);
const response = await fetch( const allProjects = await validateGitlabProvider(gitlabProvider);
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,
},
},
);
if (!response.ok) { const filteredRepos = allProjects.filter((repo: any) => {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Failed to fetch repositories: ${response.statusText}`,
});
}
const repositories = await response.json();
const filteredRepos = repositories.filter((repo: any) => {
const { full_path, kind } = repo.namespace; const { full_path, kind } = repo.namespace;
const groupName = gitlabProvider.groupName?.toLowerCase(); const groupName = gitlabProvider.groupName?.toLowerCase();
if (groupName) { if (groupName) {
const isIncluded = groupName const isIncluded = groupName
.split(",") .split(",")
.some((name) => full_path.toLowerCase().includes(name)); .some((name) =>
full_path.toLowerCase().startsWith(name.trim().toLowerCase()),
);
return isIncluded && kind === "group"; return isIncluded && kind === "group";
} }
@@ -432,23 +418,7 @@ export const testGitlabConnection = async (
const gitlabProvider = await findGitlabById(gitlabId); const gitlabProvider = await findGitlabById(gitlabId);
const response = await fetch( const repositories = await validateGitlabProvider(gitlabProvider);
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,
},
},
);
if (!response.ok) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Failed to fetch repositories: ${response.statusText}`,
});
}
const repositories = await response.json();
const filteredRepos = repositories.filter((repo: any) => { const filteredRepos = repositories.filter((repo: any) => {
const { full_path, kind } = repo.namespace; const { full_path, kind } = repo.namespace;
@@ -456,10 +426,56 @@ export const testGitlabConnection = async (
if (groupName) { if (groupName) {
return groupName return groupName
.split(",") .split(",")
.some((name) => full_path.toLowerCase().includes(name)); .some((name) =>
full_path.toLowerCase().startsWith(name.trim().toLowerCase()),
);
} }
return kind === "user"; return kind === "user";
}); });
return filteredRepos.length; return filteredRepos.length;
}; };
export const validateGitlabProvider = async (gitlabProvider: Gitlab) => {
try {
const allProjects = [];
let page = 1;
const perPage = 100; // GitLab's max per page is 100
while (true) {
const response = await fetch(
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${page}&per_page=${perPage}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,
},
},
);
if (!response.ok) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Failed to fetch repositories: ${response.statusText}`,
});
}
const projects = await response.json();
if (projects.length === 0) {
break;
}
allProjects.push(...projects);
page++;
const total = response.headers.get("x-total");
if (total && allProjects.length >= Number.parseInt(total)) {
break;
}
}
return allProjects;
} catch (error) {
throw error;
}
};

View File

@@ -1,10 +1,10 @@
import type { Destination } from "@dokploy/server/services/destination"; import type { apiRestoreBackup } from "@dokploy/server/db/schema";
import type { Compose } from "@dokploy/server/services/compose"; import type { Compose } from "@dokploy/server/services/compose";
import type { Destination } from "@dokploy/server/services/destination";
import type { z } from "zod";
import { getS3Credentials } from "../backups/utils"; import { getS3Credentials } from "../backups/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getRestoreCommand } from "./utils"; import { getRestoreCommand } from "./utils";
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
import type { z } from "zod";
interface DatabaseCredentials { interface DatabaseCredentials {
databaseUser?: string; databaseUser?: string;

View File

@@ -1,10 +1,10 @@
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
import type { Destination } from "@dokploy/server/services/destination"; import type { Destination } from "@dokploy/server/services/destination";
import type { Mariadb } from "@dokploy/server/services/mariadb"; import type { Mariadb } from "@dokploy/server/services/mariadb";
import type { z } from "zod";
import { getS3Credentials } from "../backups/utils"; import { getS3Credentials } from "../backups/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getRestoreCommand } from "./utils"; import { getRestoreCommand } from "./utils";
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
import type { z } from "zod";
export const restoreMariadbBackup = async ( export const restoreMariadbBackup = async (
mariadb: Mariadb, mariadb: Mariadb,

View File

@@ -1,10 +1,10 @@
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
import type { Destination } from "@dokploy/server/services/destination"; import type { Destination } from "@dokploy/server/services/destination";
import type { Mongo } from "@dokploy/server/services/mongo"; import type { Mongo } from "@dokploy/server/services/mongo";
import type { z } from "zod";
import { getS3Credentials } from "../backups/utils"; import { getS3Credentials } from "../backups/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getRestoreCommand } from "./utils"; import { getRestoreCommand } from "./utils";
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
import type { z } from "zod";
export const restoreMongoBackup = async ( export const restoreMongoBackup = async (
mongo: Mongo, mongo: Mongo,

View File

@@ -1,10 +1,10 @@
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
import type { Destination } from "@dokploy/server/services/destination"; import type { Destination } from "@dokploy/server/services/destination";
import type { MySql } from "@dokploy/server/services/mysql"; import type { MySql } from "@dokploy/server/services/mysql";
import type { z } from "zod";
import { getS3Credentials } from "../backups/utils"; import { getS3Credentials } from "../backups/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getRestoreCommand } from "./utils"; import { getRestoreCommand } from "./utils";
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
import type { z } from "zod";
export const restoreMySqlBackup = async ( export const restoreMySqlBackup = async (
mysql: MySql, mysql: MySql,

View File

@@ -1,10 +1,10 @@
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
import type { Destination } from "@dokploy/server/services/destination"; import type { Destination } from "@dokploy/server/services/destination";
import type { Postgres } from "@dokploy/server/services/postgres"; import type { Postgres } from "@dokploy/server/services/postgres";
import type { z } from "zod";
import { getS3Credentials } from "../backups/utils"; import { getS3Credentials } from "../backups/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getRestoreCommand } from "./utils"; import { getRestoreCommand } from "./utils";
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
import type { z } from "zod";
export const restorePostgresBackup = async ( export const restorePostgresBackup = async (
postgres: Postgres, postgres: Postgres,

View File

@@ -1,10 +1,10 @@
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { IS_CLOUD, paths } from "@dokploy/server/constants";
import type { Destination } from "@dokploy/server/services/destination"; import type { Destination } from "@dokploy/server/services/destination";
import { getS3Credentials } from "../backups/utils"; import { getS3Credentials } from "../backups/utils";
import { execAsync } from "../process/execAsync"; import { execAsync } from "../process/execAsync";
import { paths, IS_CLOUD } from "@dokploy/server/constants";
import { mkdtemp } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
export const restoreWebServerBackup = async ( export const restoreWebServerBackup = async (
destination: Destination, destination: Destination,

View File

@@ -1,6 +1,6 @@
import { db } from "../../db/index";
import { schedules } from "@dokploy/server/db/schema"; import { schedules } from "@dokploy/server/db/schema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { db } from "../../db/index";
import { scheduleJob } from "./utils"; import { scheduleJob } from "./utils";
export const initSchedules = async () => { export const initSchedules = async () => {

View File

@@ -1,14 +1,14 @@
import { createWriteStream } from "node:fs";
import path from "node:path";
import { paths } from "@dokploy/server/constants";
import type { Schedule } from "@dokploy/server/db/schema/schedule"; import type { Schedule } from "@dokploy/server/db/schema/schedule";
import { createDeploymentSchedule } from "@dokploy/server/services/deployment";
import { updateDeploymentStatus } from "@dokploy/server/services/deployment";
import { findScheduleById } from "@dokploy/server/services/schedule"; import { findScheduleById } from "@dokploy/server/services/schedule";
import { scheduledJobs, scheduleJob as scheduleJobNode } from "node-schedule"; import { scheduleJob as scheduleJobNode, scheduledJobs } from "node-schedule";
import { getComposeContainer, getServiceContainer } from "../docker/utils"; import { getComposeContainer, getServiceContainer } from "../docker/utils";
import { execAsyncRemote } from "../process/execAsync"; import { execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync"; import { spawnAsync } from "../process/spawnAsync";
import { createDeploymentSchedule } from "@dokploy/server/services/deployment";
import { createWriteStream } from "node:fs";
import { updateDeploymentStatus } from "@dokploy/server/services/deployment";
import { paths } from "@dokploy/server/constants";
import path from "node:path";
export const scheduleJob = (schedule: Schedule) => { export const scheduleJob = (schedule: Schedule) => {
const { cronExpression, scheduleId } = schedule; const { cronExpression, scheduleId } = schedule;

6112
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff