mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-05 22:15:22 +02:00
Compare commits
282 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a93f18eb4a | ||
|
|
77e9617770 | ||
|
|
21e97b0175 | ||
|
|
a6618a14d5 | ||
|
|
59308ab013 | ||
|
|
e19c8d7a7a | ||
|
|
421c93795b | ||
|
|
182f908c31 | ||
|
|
20616363e9 | ||
|
|
d85073b26d | ||
|
|
303d1b1b87 | ||
|
|
60d4e1ba63 | ||
|
|
83d52b68f0 | ||
|
|
af3b1a27f4 | ||
|
|
7f94593c07 | ||
|
|
5df7654873 | ||
|
|
054836fd4c | ||
|
|
484ead1f1f | ||
|
|
fbada4c5de | ||
|
|
491113416b | ||
|
|
c42f5cb799 | ||
|
|
47aa223f87 | ||
|
|
cb586c9b74 | ||
|
|
554ac59b97 | ||
|
|
0247898876 | ||
|
|
467acc4d4d | ||
|
|
fc2778db35 | ||
|
|
85d6ff9012 | ||
|
|
fa053b4d1f | ||
|
|
522f8baec7 | ||
|
|
bcc7afa3e4 | ||
|
|
647a5d05a6 | ||
|
|
e15d41f80d | ||
|
|
6c7c919d49 | ||
|
|
22eb965919 | ||
|
|
c0746b95b3 | ||
|
|
dfdedf9e48 | ||
|
|
7e76eb4dd1 | ||
|
|
c1f777e23e | ||
|
|
975d13c7e1 | ||
|
|
017bdd2778 | ||
|
|
01e5cf0852 | ||
|
|
548df8c0f4 | ||
|
|
8faa6ae1cf | ||
|
|
76ed1107c2 | ||
|
|
cff5049096 | ||
|
|
cb5ca100a6 | ||
|
|
03d1e974dd | ||
|
|
1e6dbb5e8e | ||
|
|
431dadb6c2 | ||
|
|
22e42b62ad | ||
|
|
49ee8ce132 | ||
|
|
b609d72d1c | ||
|
|
d7071fba60 | ||
|
|
76585991ec | ||
|
|
da6efcf733 | ||
|
|
64b0770cfb | ||
|
|
1ec83a3236 | ||
|
|
9bace8e58b | ||
|
|
c0afcaf3f6 | ||
|
|
53edf06476 | ||
|
|
255e9e4095 | ||
|
|
03f923c6e2 | ||
|
|
4685ef7439 | ||
|
|
626cfb80b4 | ||
|
|
9591fbff08 | ||
|
|
fbda00f059 | ||
|
|
1907e7e59c | ||
|
|
ffe7b04bea | ||
|
|
fe0a662afd | ||
|
|
df9fad088f | ||
|
|
319584d911 | ||
|
|
7d5a660f4d | ||
|
|
f7f0cbf318 | ||
|
|
841c0731aa | ||
|
|
137cd25267 | ||
|
|
2d40b2dfe5 | ||
|
|
e32b713742 | ||
|
|
4dcd16c41e | ||
|
|
60497fe59d | ||
|
|
8536945a60 | ||
|
|
e0a8d8258c | ||
|
|
988357fb55 | ||
|
|
fe19cdb5e4 | ||
|
|
c4654a9619 | ||
|
|
0a123a652b | ||
|
|
5d437c29b2 | ||
|
|
53f345ab1d | ||
|
|
2644b638d1 | ||
|
|
8785282133 | ||
|
|
35c084af1d | ||
|
|
b63488baba | ||
|
|
7e5f21b28e | ||
|
|
8d41bafb93 | ||
|
|
6f5049efd5 | ||
|
|
6dd6b636e5 | ||
|
|
8488d530f3 | ||
|
|
6c61d5cdf5 | ||
|
|
8036455c2d | ||
|
|
bf44eeab3d | ||
|
|
0cd185696d | ||
|
|
339697437a | ||
|
|
e0b15fe971 | ||
|
|
afc5ea43da | ||
|
|
bb337e819e | ||
|
|
160dd10f77 | ||
|
|
49b096fef6 | ||
|
|
4828b840cb | ||
|
|
67af70448b | ||
|
|
946acf5245 | ||
|
|
390b3a835a | ||
|
|
2842bf9a91 | ||
|
|
8a0e10f6f4 | ||
|
|
67efa82b91 | ||
|
|
546d6b87ea | ||
|
|
d012d19253 | ||
|
|
e925ed9ea4 | ||
|
|
0c05809d7d | ||
|
|
29f1631950 | ||
|
|
9c0c58035a | ||
|
|
f4262569dd | ||
|
|
99cf6eae49 | ||
|
|
0488546706 | ||
|
|
dc32cd71e5 | ||
|
|
efdfd5d13c | ||
|
|
d31cab76f0 | ||
|
|
00ed202127 | ||
|
|
aaa4ca297d | ||
|
|
629871e683 | ||
|
|
a237c651c3 | ||
|
|
4f9b0d9d59 | ||
|
|
b701a0b504 | ||
|
|
39036202bb | ||
|
|
4225ad83e7 | ||
|
|
38c4e0ede1 | ||
|
|
25a64c703f | ||
|
|
ab5871add7 | ||
|
|
2b0e009f6a | ||
|
|
9b6ea99eea | ||
|
|
c4cf545d85 | ||
|
|
1edd717432 | ||
|
|
5b88af6158 | ||
|
|
e995d894d8 | ||
|
|
5f56512e56 | ||
|
|
24e4930fc1 | ||
|
|
541728805f | ||
|
|
9e4bac1386 | ||
|
|
ed8d32d050 | ||
|
|
7fb66bc58b | ||
|
|
58c06fba86 | ||
|
|
3cf27a068a | ||
|
|
7cfbea3f60 | ||
|
|
7907e33431 | ||
|
|
89f3078ce5 | ||
|
|
f3ce69b656 | ||
|
|
43555cdabe | ||
|
|
651bf3a303 | ||
|
|
4cde1a8a7d | ||
|
|
405efcac0b | ||
|
|
8397de0dca | ||
|
|
06b58e6495 | ||
|
|
24db4006cf | ||
|
|
84009c5e9b | ||
|
|
e32afde973 | ||
|
|
fecffac573 | ||
|
|
b4448e013c | ||
|
|
c3f06a6272 | ||
|
|
2be724f780 | ||
|
|
bf78326c96 | ||
|
|
4ca8722c6e | ||
|
|
e56e1eb687 | ||
|
|
5a5c302bdc | ||
|
|
997dc85985 | ||
|
|
09ef851372 | ||
|
|
7c4987d84d | ||
|
|
5cebf5540a | ||
|
|
3df2f8e58c | ||
|
|
a642d36a23 | ||
|
|
72bceec62d | ||
|
|
3d32314e80 | ||
|
|
7259830ac1 | ||
|
|
daa87c0dc7 | ||
|
|
a2ee55e0e9 | ||
|
|
de6aeac243 | ||
|
|
f640b4a87f | ||
|
|
3747db08d4 | ||
|
|
ab4677ac0e | ||
|
|
172d55311e | ||
|
|
3ce25e2ac8 | ||
|
|
388ded9aa5 | ||
|
|
767d3e1944 | ||
|
|
ec1d6c7430 | ||
|
|
8abeae5e63 | ||
|
|
cc90d9ec9b | ||
|
|
6b5de00fb0 | ||
|
|
5ed96fb0ce | ||
|
|
a73af1d578 | ||
|
|
5867a27901 | ||
|
|
8f7bffc349 | ||
|
|
21ee22d4f5 | ||
|
|
fb72132a4b | ||
|
|
ca904c15d9 | ||
|
|
682863f83e | ||
|
|
acd722678e | ||
|
|
3750977f41 | ||
|
|
9b401059b0 | ||
|
|
6a3ef5c860 | ||
|
|
a36518a8f0 | ||
|
|
a5eb4b0a72 | ||
|
|
b5c0876dd4 | ||
|
|
9745d12ac8 | ||
|
|
5c72e5a452 | ||
|
|
600f4b2106 | ||
|
|
c12d37fe0a | ||
|
|
bba8d00ba2 | ||
|
|
78665ffbfa | ||
|
|
d41c8c70c3 | ||
|
|
f13e5d449c | ||
|
|
d256998677 | ||
|
|
4aaf04ce74 | ||
|
|
ecfca9419a | ||
|
|
dfd6764320 | ||
|
|
73bf5274f5 | ||
|
|
fc38a42587 | ||
|
|
9b255964fe | ||
|
|
29f55ca1a0 | ||
|
|
6a5fb8faff | ||
|
|
5c225c8d42 | ||
|
|
c1c5fc978b | ||
|
|
ffd19f591d | ||
|
|
81a41a7f31 | ||
|
|
1c9b704ecc | ||
|
|
edf1fdedf0 | ||
|
|
8484649071 | ||
|
|
1e68248611 | ||
|
|
b3e35c5838 | ||
|
|
ddd4ba8135 | ||
|
|
539544d0de | ||
|
|
123b5d098b | ||
|
|
796a9ca11f | ||
|
|
2872ef3ccb | ||
|
|
06a772e344 | ||
|
|
e99666f4c0 | ||
|
|
bd243d79e2 | ||
|
|
18b4b23f79 | ||
|
|
071a9d5104 | ||
|
|
61ebd1b16e | ||
|
|
9836c988a0 | ||
|
|
03d7738032 | ||
|
|
98aa474975 | ||
|
|
7bd6b66551 | ||
|
|
727e50648e | ||
|
|
0b2b20caeb | ||
|
|
6cc64b4454 | ||
|
|
349bc89851 | ||
|
|
7046d05f63 | ||
|
|
cef21ac8b5 | ||
|
|
2ae7e562bb | ||
|
|
e4b998c608 | ||
|
|
9b7aacc934 | ||
|
|
7027f39c48 | ||
|
|
9f6f872536 | ||
|
|
cb03b153ac | ||
|
|
e5d7a0cb10 | ||
|
|
bf65bc9462 | ||
|
|
b48b9765cd | ||
|
|
7cce02f74d | ||
|
|
3dc3672406 | ||
|
|
bbfe095045 | ||
|
|
65c1001751 | ||
|
|
5212bde021 | ||
|
|
9059f42b03 | ||
|
|
3b9f5d6f5c | ||
|
|
0aff344bc0 | ||
|
|
4715f34e15 | ||
|
|
59386ed4b7 | ||
|
|
21dee4abac | ||
|
|
5b0bf99cbf | ||
|
|
8e227a3286 | ||
|
|
869e58739f | ||
|
|
f8721d3e04 | ||
|
|
a6c7c3b031 |
@@ -11,6 +11,7 @@ jobs:
|
|||||||
command: |
|
command: |
|
||||||
cp apps/dokploy/.env.production.example .env.production
|
cp apps/dokploy/.env.production.example .env.production
|
||||||
cp apps/dokploy/.env.production.example apps/dokploy/.env.production
|
cp apps/dokploy/.env.production.example apps/dokploy/.env.production
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: Build and push AMD64 image
|
name: Build and push AMD64 image
|
||||||
command: |
|
command: |
|
||||||
@@ -61,7 +62,7 @@ jobs:
|
|||||||
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
|
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
|
||||||
echo $VERSION
|
echo $VERSION
|
||||||
TAG="latest"
|
TAG="latest"
|
||||||
|
|
||||||
docker manifest create dokploy/dokploy:${TAG} \
|
docker manifest create dokploy/dokploy:${TAG} \
|
||||||
dokploy/dokploy:${TAG}-amd64 \
|
dokploy/dokploy:${TAG}-amd64 \
|
||||||
dokploy/dokploy:${TAG}-arm64
|
dokploy/dokploy:${TAG}-arm64
|
||||||
|
|||||||
BIN
.github/sponsors/lxaer.png
vendored
BIN
.github/sponsors/lxaer.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 267 KiB After Width: | Height: | Size: 248 KiB |
73
.github/workflows/deploy.yml
vendored
73
.github/workflows/deploy.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Build Docs & Website Docker images
|
name: Build Docker images
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -48,3 +48,74 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: dokploy/website:latest
|
tags: dokploy/website:latest
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
|
|
||||||
|
|
||||||
|
build-and-push-cloud-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile.cloud
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
siumauricio/cloud:${{ github.ref_name == 'main' && 'main' || 'canary' }}
|
||||||
|
platforms: linux/amd64
|
||||||
|
|
||||||
|
build-and-push-schedule-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile.schedule
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
siumauricio/schedule:${{ github.ref_name == 'main' && 'main' || 'canary' }}
|
||||||
|
platforms: linux/amd64
|
||||||
|
|
||||||
|
|
||||||
|
build-and-push-server-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile.server
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
siumauricio/server:${{ github.ref_name == 'main' && 'main' || 'canary' }}
|
||||||
|
platforms: linux/amd64
|
||||||
3
.github/workflows/pull-request.yml
vendored
3
.github/workflows/pull-request.yml
vendored
@@ -18,6 +18,7 @@ jobs:
|
|||||||
node-version: 18.18.0
|
node-version: 18.18.0
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
|
- run: pnpm run server:build
|
||||||
- run: pnpm biome ci
|
- run: pnpm biome ci
|
||||||
- run: pnpm typecheck
|
- run: pnpm typecheck
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ jobs:
|
|||||||
node-version: 18.18.0
|
node-version: 18.18.0
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
|
- run: pnpm run server:build
|
||||||
- run: pnpm build
|
- run: pnpm build
|
||||||
|
|
||||||
parallel-tests:
|
parallel-tests:
|
||||||
@@ -44,4 +46,5 @@ jobs:
|
|||||||
node-version: 18.18.0
|
node-version: 18.18.0
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
|
- run: pnpm run server:build
|
||||||
- run: pnpm test
|
- run: pnpm test
|
||||||
|
|||||||
1
.husky/commit-msg
Normal file
1
.husky/commit-msg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
npx commitlint --edit "$1"
|
||||||
6
.husky/install.mjs
Normal file
6
.husky/install.mjs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// Skip Husky install in production and CI
|
||||||
|
if (process.env.NODE_ENV === "production" || process.env.CI === "true") {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
const husky = (await import("husky")).default;
|
||||||
|
console.log(husky());
|
||||||
2
.husky/pre-commit
Normal file
2
.husky/pre-commit
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pnpm run check
|
||||||
|
git add .
|
||||||
@@ -71,6 +71,11 @@ Run the command that will spin up all the required services and files.
|
|||||||
pnpm run dokploy:setup
|
pnpm run dokploy:setup
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Run this script
|
||||||
|
```bash
|
||||||
|
pnpm run server:script
|
||||||
|
```
|
||||||
|
|
||||||
Now run the development server.
|
Now run the development server.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
|||||||
# Deploy only the dokploy app
|
# Deploy only the dokploy app
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
RUN pnpm --filter=@dokploy/server switch:prod
|
||||||
|
RUN pnpm --filter=@dokploy/server build
|
||||||
RUN pnpm --filter=./apps/dokploy run build
|
RUN pnpm --filter=./apps/dokploy run build
|
||||||
|
|
||||||
RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy
|
RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy
|
||||||
|
|
||||||
RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next
|
RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next
|
||||||
|
|||||||
53
Dockerfile.cloud
Normal file
53
Dockerfile.cloud
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
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 --filter=@dokploy/server --filter=./apps/dokploy install --frozen-lockfile
|
||||||
|
|
||||||
|
# Deploy only the dokploy app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
RUN pnpm --filter=@dokploy/server switch:prod
|
||||||
|
RUN pnpm --filter=@dokploy/server build
|
||||||
|
RUN pnpm --filter=./apps/dokploy run build
|
||||||
|
|
||||||
|
RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy
|
||||||
|
|
||||||
|
RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next
|
||||||
|
RUN cp -R /usr/src/app/apps/dokploy/dist /prod/dokploy/dist
|
||||||
|
|
||||||
|
FROM base AS dokploy
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Set production
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy only the necessary files
|
||||||
|
COPY --from=build /prod/dokploy/.next ./.next
|
||||||
|
COPY --from=build /prod/dokploy/dist ./dist
|
||||||
|
COPY --from=build /prod/dokploy/next.config.mjs ./next.config.mjs
|
||||||
|
COPY --from=build /prod/dokploy/public ./public
|
||||||
|
COPY --from=build /prod/dokploy/package.json ./package.json
|
||||||
|
COPY --from=build /prod/dokploy/drizzle ./drizzle
|
||||||
|
COPY --from=build /prod/dokploy/components.json ./components.json
|
||||||
|
COPY --from=build /prod/dokploy/node_modules ./node_modules
|
||||||
|
|
||||||
|
|
||||||
|
# Install RCLONE
|
||||||
|
RUN curl https://rclone.org/install.sh | bash
|
||||||
|
|
||||||
|
# tsx
|
||||||
|
RUN pnpm install -g tsx
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD [ "pnpm", "start" ]
|
||||||
37
Dockerfile.schedule
Normal file
37
Dockerfile.schedule
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
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 --filter=@dokploy/server --filter=./apps/schedules install --frozen-lockfile
|
||||||
|
|
||||||
|
# Deploy only the dokploy app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
RUN pnpm --filter=@dokploy/server switch:prod
|
||||||
|
RUN pnpm --filter=@dokploy/server build
|
||||||
|
RUN pnpm --filter=./apps/schedules run build
|
||||||
|
|
||||||
|
RUN pnpm --filter=./apps/schedules --prod deploy /prod/schedules
|
||||||
|
|
||||||
|
RUN cp -R /usr/src/app/apps/schedules/dist /prod/schedules/dist
|
||||||
|
|
||||||
|
FROM base AS dokploy
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Set production
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Copy only the necessary files
|
||||||
|
COPY --from=build /prod/schedules/dist ./dist
|
||||||
|
COPY --from=build /prod/schedules/package.json ./package.json
|
||||||
|
COPY --from=build /prod/schedules/node_modules ./node_modules
|
||||||
|
|
||||||
|
CMD HOSTNAME=0.0.0.0 && pnpm start
|
||||||
37
Dockerfile.server
Normal file
37
Dockerfile.server
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
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 --filter=@dokploy/server --filter=./apps/api install --frozen-lockfile
|
||||||
|
|
||||||
|
# Deploy only the dokploy app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
RUN pnpm --filter=@dokploy/server switch:prod
|
||||||
|
RUN pnpm --filter=@dokploy/server build
|
||||||
|
RUN pnpm --filter=./apps/api run build
|
||||||
|
|
||||||
|
RUN pnpm --filter=./apps/api --prod deploy /prod/api
|
||||||
|
|
||||||
|
RUN cp -R /usr/src/app/apps/api/dist /prod/api/dist
|
||||||
|
|
||||||
|
FROM base AS dokploy
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Set production
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Copy only the necessary files
|
||||||
|
COPY --from=build /prod/api/dist ./dist
|
||||||
|
COPY --from=build /prod/api/package.json ./package.json
|
||||||
|
COPY --from=build /prod/api/node_modules ./node_modules
|
||||||
|
|
||||||
|
CMD HOSTNAME=0.0.0.0 && pnpm start
|
||||||
@@ -17,10 +17,10 @@ See the License for the specific language governing permissions and limitations
|
|||||||
|
|
||||||
## Additional Terms for Specific Features
|
## Additional Terms for Specific Features
|
||||||
|
|
||||||
The following additional terms apply to the multi-node support and Docker Compose file support features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
The following additional terms apply to the multi-node support, Docker Compose file and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
||||||
|
|
||||||
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support and Docker Compose file support, will always be free to use in the self-hosted version.
|
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support and Multi Server, will always be free to use in the self-hosted version.
|
||||||
- **Restriction on Resale**: The multi-node support and Docker Compose file support features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
- **Restriction on Resale**: The multi-node support, Docker Compose file support and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
||||||
- **Modification Distribution**: Any modifications to the multi-node support and Docker Compose file support features must be distributed freely and cannot be sold or offered as a service.
|
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support and Multi Server features must be distributed freely and cannot be sold or offered as a service.
|
||||||
|
|
||||||
For further inquiries or permissions, please contact us directly.
|
For further inquiries or permissions, please contact us directly.
|
||||||
|
|||||||
33
README.md
33
README.md
@@ -15,28 +15,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
Dokploy is a free self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
|
Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
Dokploy include multiples features to make your life easier.
|
Dokploy includes multiple features to make your life easier.
|
||||||
|
|
||||||
- **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.).
|
- **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.).
|
||||||
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, Redis.
|
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, and Redis.
|
||||||
- **Backups**: Automate backups for databases to a external storage destination.
|
- **Backups**: Automate backups for databases to an external storage destination.
|
||||||
- **Docker Compose**: Native support for Docker Compose to manage complex applications.
|
- **Docker Compose**: Native support for Docker Compose to manage complex applications.
|
||||||
- **Multi Node**: Scale applications to multiples nodes using docker swarm to manage the cluster.
|
- **Multi Node**: Scale applications to multiple nodes using Docker Swarm to manage the cluster.
|
||||||
- **Templates**: Deploy in a single click open source templates (Plausible, Pocketbase, Calcom, etc.).
|
- **Templates**: Deploy open-source templates (Plausible, Pocketbase, Calcom, etc.) with a single click.
|
||||||
- **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
|
- **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
|
||||||
- **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage, for every resource.
|
- **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage for every resource.
|
||||||
- **Docker Management**: Easily deploy and manage Docker containers.
|
- **Docker Management**: Easily deploy and manage Docker containers.
|
||||||
- **CLI/API**: Manage your applications and databases using the command line or trought the API.
|
- **CLI/API**: Manage your applications and databases using the command line or through the API.
|
||||||
- **Notifications**: Get notified when your deployments are successful or failed (Slack, Discord, Telegram, Email, etc.)
|
- **Notifications**: Get notified when your deployments succeed or fail (via Slack, Discord, Telegram, Email, etc.).
|
||||||
|
- **Multi Server**: Deploy and manage your applications remotely to external servers.
|
||||||
- **Self-Hosted**: Self-host Dokploy on your VPS.
|
- **Self-Hosted**: Self-host Dokploy on your VPS.
|
||||||
|
|
||||||
## 🚀 Getting Started
|
## 🚀 Getting Started
|
||||||
|
|
||||||
To get started run the following command in a VPS:
|
To get started, run the following command on a VPS:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sSL https://dokploy.com/install.sh | sh
|
curl -sSL https://dokploy.com/install.sh | sh
|
||||||
@@ -58,7 +59,14 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
|
|
||||||
### Hero Sponsors 🎖
|
### Hero Sponsors 🎖
|
||||||
|
|
||||||
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy" target="_blank" ><img src=".github/sponsors/hostinger.jpg" alt="Hostinger" width="200"/></a>
|
<div style="display: flex; align-items: center; gap: 20px;">
|
||||||
|
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy" target="_blank" style="display: inline-block;">
|
||||||
|
<img src=".github/sponsors/hostinger.jpg" alt="Hostinger" height="50"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.lxaer.com/?ref=dokploy" target="_blank" style="display: inline-block;">
|
||||||
|
<img src=".github/sponsors/lxaer.png" alt="LX Aer" height="50"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
### Premium Supporters 🥇
|
### Premium Supporters 🥇
|
||||||
|
|
||||||
@@ -81,6 +89,9 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
|
|
||||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||||
<a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
|
<a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
|
||||||
|
<a href="https://rivo.gg/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/126797452?s=200&v=4" width="60px" alt="Rivo.gg"/></a>
|
||||||
|
<a href="https://photoquest.wedding/?ref=dokploy"><img src="https://photoquest.wedding/favicon/android-chrome-512x512.png" width="60px" alt="Rivo.gg"/></a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
#### Organizations:
|
#### Organizations:
|
||||||
|
|||||||
@@ -1,15 +1,33 @@
|
|||||||
{
|
{
|
||||||
"name": "my-app",
|
"name": "@dokploy/api",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx watch src/index.ts"
|
"dev": "PORT=4000 tsx watch src/index.ts",
|
||||||
|
"build": "tsc --project tsconfig.json",
|
||||||
|
"start": "node --experimental-specifier-resolution=node dist/index.js",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"pino": "9.4.0",
|
||||||
|
"pino-pretty": "11.2.2",
|
||||||
|
"@hono/zod-validator": "0.3.0",
|
||||||
|
"zod": "^3.23.4",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"@dokploy/server": "workspace:*",
|
||||||
"@hono/node-server": "^1.12.1",
|
"@hono/node-server": "^1.12.1",
|
||||||
"hono": "^4.5.8",
|
"hono": "^4.5.8",
|
||||||
"dotenv": "^16.3.1"
|
"dotenv": "^16.3.1",
|
||||||
|
"redis": "4.7.0",
|
||||||
|
"@nerimity/mimiqueue": "1.2.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"typescript": "^5.4.2",
|
||||||
|
"@types/react": "^18.2.37",
|
||||||
|
"@types/react-dom": "^18.2.15",
|
||||||
"@types/node": "^20.11.17",
|
"@types/node": "^20.11.17",
|
||||||
"tsx": "^4.7.1"
|
"tsx": "^4.7.1"
|
||||||
}
|
},
|
||||||
|
"packageManager": "pnpm@9.5.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,66 +1,61 @@
|
|||||||
import { serve } from "@hono/node-server";
|
import { serve } from "@hono/node-server";
|
||||||
import { config } from "dotenv";
|
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { cors } from "hono/cors";
|
import "dotenv/config";
|
||||||
import { validateLemonSqueezyLicense } from "./utils";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
|
import { Queue } from "@nerimity/mimiqueue";
|
||||||
config();
|
import { createClient } from "redis";
|
||||||
|
import { logger } from "./logger";
|
||||||
|
import { type DeployJob, deployJobSchema } from "./schema";
|
||||||
|
import { deploy } from "./utils";
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
const redisClient = createClient({
|
||||||
app.use(
|
url: process.env.REDIS_URL,
|
||||||
"/*",
|
|
||||||
cors({
|
|
||||||
origin: ["http://localhost:3000", "http://localhost:3001"], // Ajusta esto a los orígenes de tu aplicación Next.js
|
|
||||||
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
||||||
allowHeaders: ["Content-Type", "Authorization"],
|
|
||||||
exposeHeaders: ["Content-Length", "X-Kuma-Revision"],
|
|
||||||
maxAge: 600,
|
|
||||||
credentials: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const LEMON_SQUEEZY_API_KEY = process.env.LEMON_SQUEEZY_API_KEY;
|
|
||||||
export const LEMON_SQUEEZY_STORE_ID = process.env.LEMON_SQUEEZY_STORE_ID;
|
|
||||||
|
|
||||||
app.get("/v1/health", (c) => {
|
|
||||||
return c.text("Hello Hono!");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/v1/validate-license", async (c) => {
|
app.use(async (c, next) => {
|
||||||
const { licenseKey } = await c.req.json();
|
if (c.req.path === "/health") {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
const authHeader = c.req.header("X-API-Key");
|
||||||
|
|
||||||
if (!licenseKey) {
|
if (process.env.API_KEY !== authHeader) {
|
||||||
return c.json({ error: "License key is required" }, 400);
|
return c.json({ message: "Invalid API Key" }, 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
return next();
|
||||||
const licenseValidation = await validateLemonSqueezyLicense(licenseKey);
|
|
||||||
|
|
||||||
if (licenseValidation.valid) {
|
|
||||||
return c.json({
|
|
||||||
valid: true,
|
|
||||||
message: "License is valid",
|
|
||||||
metadata: licenseValidation.meta,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return c.json(
|
|
||||||
{
|
|
||||||
valid: false,
|
|
||||||
message: licenseValidation.error || "Invalid license",
|
|
||||||
},
|
|
||||||
400,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error during license validation:", error);
|
|
||||||
return c.json({ error: "Internal server error" }, 500);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const port = 4000;
|
app.post("/deploy", zValidator("json", deployJobSchema), (c) => {
|
||||||
console.log(`Server is running on port ${port}`);
|
const data = c.req.valid("json");
|
||||||
|
const res = queue.add(data, { groupName: data.serverId });
|
||||||
serve({
|
return c.json(
|
||||||
fetch: app.fetch,
|
{
|
||||||
port,
|
message: "Deployment Added",
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get("/health", async (c) => {
|
||||||
|
return c.json({ status: "ok" });
|
||||||
|
});
|
||||||
|
|
||||||
|
const queue = new Queue({
|
||||||
|
name: "deployments",
|
||||||
|
process: async (job: DeployJob) => {
|
||||||
|
logger.info("Deploying job", job);
|
||||||
|
return await deploy(job);
|
||||||
|
},
|
||||||
|
redisClient,
|
||||||
|
});
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
await redisClient.connect();
|
||||||
|
await redisClient.flushAll();
|
||||||
|
logger.info("Redis Cleaned");
|
||||||
|
})();
|
||||||
|
|
||||||
|
const port = Number.parseInt(process.env.PORT || "3000");
|
||||||
|
logger.info("Starting Deployments Server ✅", port);
|
||||||
|
serve({ fetch: app.fetch, port });
|
||||||
|
|||||||
10
apps/api/src/logger.ts
Normal file
10
apps/api/src/logger.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import pino from "pino";
|
||||||
|
|
||||||
|
export const logger = pino({
|
||||||
|
transport: {
|
||||||
|
target: "pino-pretty",
|
||||||
|
options: {
|
||||||
|
colorize: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
24
apps/api/src/schema.ts
Normal file
24
apps/api/src/schema.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||||
|
z.object({
|
||||||
|
applicationId: z.string(),
|
||||||
|
titleLog: z.string(),
|
||||||
|
descriptionLog: z.string(),
|
||||||
|
server: z.boolean().optional(),
|
||||||
|
type: z.enum(["deploy", "redeploy"]),
|
||||||
|
applicationType: z.literal("application"),
|
||||||
|
serverId: z.string().min(1),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
composeId: z.string(),
|
||||||
|
titleLog: z.string(),
|
||||||
|
descriptionLog: z.string(),
|
||||||
|
server: z.boolean().optional(),
|
||||||
|
type: z.enum(["deploy", "redeploy"]),
|
||||||
|
applicationType: z.literal("compose"),
|
||||||
|
serverId: z.string().min(1),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type DeployJob = z.infer<typeof deployJobSchema>;
|
||||||
@@ -1,28 +1,96 @@
|
|||||||
import { LEMON_SQUEEZY_API_KEY, LEMON_SQUEEZY_STORE_ID } from ".";
|
import {
|
||||||
|
deployApplication,
|
||||||
|
deployCompose,
|
||||||
|
deployRemoteApplication,
|
||||||
|
deployRemoteCompose,
|
||||||
|
rebuildApplication,
|
||||||
|
rebuildCompose,
|
||||||
|
rebuildRemoteApplication,
|
||||||
|
rebuildRemoteCompose,
|
||||||
|
updateApplicationStatus,
|
||||||
|
updateCompose,
|
||||||
|
} from "@dokploy/server/dist";
|
||||||
|
import type { DeployJob } from "./schema";
|
||||||
import type { LemonSqueezyLicenseResponse } from "./types";
|
import type { LemonSqueezyLicenseResponse } from "./types";
|
||||||
|
|
||||||
export const validateLemonSqueezyLicense = async (
|
// const LEMON_SQUEEZY_API_KEY = process.env.LEMON_SQUEEZY_API_KEY;
|
||||||
licenseKey: string,
|
// const LEMON_SQUEEZY_STORE_ID = process.env.LEMON_SQUEEZY_STORE_ID;
|
||||||
): Promise<LemonSqueezyLicenseResponse> => {
|
// export const validateLemonSqueezyLicense = async (
|
||||||
try {
|
// licenseKey: string,
|
||||||
const response = await fetch(
|
// ): Promise<LemonSqueezyLicenseResponse> => {
|
||||||
"https://api.lemonsqueezy.com/v1/licenses/validate",
|
// try {
|
||||||
{
|
// const response = await fetch(
|
||||||
method: "POST",
|
// "https://api.lemonsqueezy.com/v1/licenses/validate",
|
||||||
headers: {
|
// {
|
||||||
"Content-Type": "application/json",
|
// method: "POST",
|
||||||
"x-api-key": LEMON_SQUEEZY_API_KEY as string,
|
// headers: {
|
||||||
},
|
// "Content-Type": "application/json",
|
||||||
body: JSON.stringify({
|
// "x-api-key": LEMON_SQUEEZY_API_KEY as string,
|
||||||
license_key: licenseKey,
|
// },
|
||||||
store_id: LEMON_SQUEEZY_STORE_ID as string,
|
// body: JSON.stringify({
|
||||||
}),
|
// license_key: licenseKey,
|
||||||
},
|
// store_id: LEMON_SQUEEZY_STORE_ID as string,
|
||||||
);
|
// }),
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
|
||||||
return response.json();
|
// return response.json();
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error("Error validating license:", error);
|
||||||
|
// return { valid: false, error: "Error validating license" };
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
export const deploy = async (job: DeployJob) => {
|
||||||
|
try {
|
||||||
|
if (job.applicationType === "application") {
|
||||||
|
await updateApplicationStatus(job.applicationId, "running");
|
||||||
|
if (job.server) {
|
||||||
|
if (job.type === "redeploy") {
|
||||||
|
await rebuildRemoteApplication({
|
||||||
|
applicationId: job.applicationId,
|
||||||
|
titleLog: job.titleLog,
|
||||||
|
descriptionLog: job.descriptionLog,
|
||||||
|
});
|
||||||
|
} else if (job.type === "deploy") {
|
||||||
|
await deployRemoteApplication({
|
||||||
|
applicationId: job.applicationId,
|
||||||
|
titleLog: job.titleLog,
|
||||||
|
descriptionLog: job.descriptionLog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (job.applicationType === "compose") {
|
||||||
|
await updateCompose(job.composeId, {
|
||||||
|
composeStatus: "running",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (job.server) {
|
||||||
|
if (job.type === "redeploy") {
|
||||||
|
await rebuildRemoteCompose({
|
||||||
|
composeId: job.composeId,
|
||||||
|
titleLog: job.titleLog,
|
||||||
|
descriptionLog: job.descriptionLog,
|
||||||
|
});
|
||||||
|
} else if (job.type === "deploy") {
|
||||||
|
await deployRemoteCompose({
|
||||||
|
composeId: job.composeId,
|
||||||
|
titleLog: job.titleLog,
|
||||||
|
descriptionLog: job.descriptionLog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error validating license:", error);
|
console.log(error);
|
||||||
return { valid: false, error: "Error validating license" };
|
if (job.applicationType === "application") {
|
||||||
|
await updateApplicationStatus(job.applicationId, "error");
|
||||||
|
} else if (job.applicationType === "compose") {
|
||||||
|
await updateCompose(job.composeId, {
|
||||||
|
composeStatus: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "Node",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"types": ["node"],
|
"outDir": "dist",
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"jsxImportSource": "hono/jsx"
|
"jsxImportSource": "hono/jsx"
|
||||||
}
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export function generateMetadata({
|
|||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: "summary_large_image",
|
card: "summary_large_image",
|
||||||
creator: "@siumauricio",
|
creator: "@getdokploy",
|
||||||
title: page.data.title,
|
title: page.data.title,
|
||||||
description: page.data.description,
|
description: page.data.description,
|
||||||
images: [
|
images: [
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ Configure the source of your code, the way your application is built, and also m
|
|||||||
|
|
||||||
If you need to assign environment variables to your application, you can do so here.
|
If you need to assign environment variables to your application, you can do so here.
|
||||||
|
|
||||||
|
In case you need to use a multiline variable, you can wrap it in double quotes just like this `'"here_is_my_private_key"'`.
|
||||||
|
|
||||||
## Monitoring
|
## Monitoring
|
||||||
|
|
||||||
Four graphs will be displayed for the use of memory, CPU, disk, and network. Note that the information is only updated if you are viewing the current page, otherwise it will not be updated.
|
Four graphs will be displayed for the use of memory, CPU, disk, and network. Note that the information is only updated if you are viewing the current page, otherwise it will not be updated.
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ Actions like deploying, updating, and deleting your database, and stopping it.
|
|||||||
|
|
||||||
If you need to assign environment variables to your application, you can do so here.
|
If you need to assign environment variables to your application, you can do so here.
|
||||||
|
|
||||||
|
In case you need to use a multiline variable, you can wrap it in double quotes just like this `'"here_is_my_private_key"'`.
|
||||||
|
|
||||||
## Monitoring
|
## Monitoring
|
||||||
|
|
||||||
Four graphs will be displayed for the use of memory, CPU, disk, and network. Note that the information is only updated if you are viewing the current page, otherwise it will not be updated.
|
Four graphs will be displayed for the use of memory, CPU, disk, and network. Note that the information is only updated if you are viewing the current page, otherwise it will not be updated.
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ description: '学习如何在服务器上手动安装 Dokploy。'
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# 确保以根用户身份运行脚本
|
# Ensure the script is run as root
|
||||||
if [ "$(id -u)" != "0" ]; then
|
if [ "$(id -u)" != "0" ]; then
|
||||||
echo "This script must be run as root" >&2
|
echo "This script must be run as root" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 检查 Linux 操作系统(非 macOS 或 Docker 容器内的操作系统)
|
# Check for Linux OS (not macOS or inside a Docker container)
|
||||||
if [ "$(uname)" = "Darwin" ]; then
|
if [ "$(uname)" = "Darwin" ]; then
|
||||||
echo "This script must be run on Linux" >&2
|
echo "This script must be run on Linux" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -32,7 +32,7 @@ if [ -f /.dockerenv ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 检查端口是否被占用
|
# Check for occupied ports
|
||||||
if ss -tulnp | grep ':80 ' >/dev/null; then
|
if ss -tulnp | grep ':80 ' >/dev/null; then
|
||||||
echo "Error: Port 80 is already in use" >&2
|
echo "Error: Port 80 is already in use" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -43,32 +43,53 @@ if ss -tulnp | grep ':443 ' >/dev/null; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 检查命令是否存在
|
# Function to check if a command exists
|
||||||
command_exists() {
|
command_exists() {
|
||||||
command -v "$@" > /dev/null 2>&1
|
command -v "$@" > /dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
# 如果未安装 Docker 则安装
|
# Install Docker if it is not installed
|
||||||
if command_exists docker; then
|
if command_exists docker; then
|
||||||
echo "Docker already installed"
|
echo "Docker already installed"
|
||||||
else
|
else
|
||||||
curl -sSL https://get.docker.com | sh
|
curl -sSL https://get.docker.com | sh
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 初始化 Docker Swarm
|
# Initialize Docker Swarm
|
||||||
docker swarm leave --force 2>/dev/null
|
docker swarm leave --force 2>/dev/null
|
||||||
advertise_addr=$(curl -s ifconfig.me)
|
|
||||||
docker swarm init --advertise-addr $advertise_addr
|
|
||||||
echo "Swarm initialized"
|
|
||||||
|
|
||||||
# 创建网络
|
get_ip() {
|
||||||
docker network rm -f dokploy-network 2>/dev/null
|
# Try to get IPv4
|
||||||
docker network create --driver overlay --attachable dokploy-network
|
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
|
||||||
echo "Network created"
|
|
||||||
|
|
||||||
# 准备配置目录
|
if [ -n "$ipv4" ]; then
|
||||||
mkdir -p /etc/dokploy
|
echo "$ipv4"
|
||||||
chmod -R 777 /etc/dokploy
|
else
|
||||||
|
# Try to get IPv6
|
||||||
|
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
|
||||||
|
if [ -n "$ipv6" ]; then
|
||||||
|
echo "$ipv6"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}"
|
||||||
|
|
||||||
|
docker swarm init --advertise-addr $advertise_addr
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error: Failed to initialize Docker Swarm" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker network rm -f dokploy-network 2>/dev/null
|
||||||
|
docker network create --driver overlay --attachable dokploy-network
|
||||||
|
|
||||||
|
echo "Network created"
|
||||||
|
|
||||||
|
mkdir -p /etc/dokploy
|
||||||
|
|
||||||
|
chmod 777 /etc/dokploy
|
||||||
|
|
||||||
# Pull and deploy Dokploy
|
# Pull and deploy Dokploy
|
||||||
docker pull dokploy/dokploy:latest
|
docker pull dokploy/dokploy:latest
|
||||||
@@ -84,9 +105,10 @@ docker service create \
|
|||||||
-e PORT=<Value For PORT eg(3000)> \
|
-e PORT=<Value For PORT eg(3000)> \
|
||||||
-e TRAEFIK_SSL_PORT=<Value For SSL PORT eg(444)> \
|
-e TRAEFIK_SSL_PORT=<Value For SSL PORT eg(444)> \
|
||||||
-e TRAEFIK_PORT=<VALUE FOR TRAEFIK HTTP PORT eg(81)> \
|
-e TRAEFIK_PORT=<VALUE FOR TRAEFIK HTTP PORT eg(81)> \
|
||||||
|
-e ADVERTISE_ADDR=$advertise_addr \
|
||||||
dokploy/dokploy:latest
|
dokploy/dokploy:latest
|
||||||
|
|
||||||
# 输出成功消息
|
# Output success message
|
||||||
GREEN="\033[0;32m"
|
GREEN="\033[0;32m"
|
||||||
YELLOW="\033[1;33m"
|
YELLOW="\033[1;33m"
|
||||||
BLUE="\033[0;34m"
|
BLUE="\033[0;34m"
|
||||||
|
|||||||
@@ -57,18 +57,39 @@ fi
|
|||||||
|
|
||||||
# Initialize Docker Swarm
|
# Initialize Docker Swarm
|
||||||
docker swarm leave --force 2>/dev/null
|
docker swarm leave --force 2>/dev/null
|
||||||
advertise_addr=$(curl -s ifconfig.me)
|
|
||||||
docker swarm init --advertise-addr $advertise_addr
|
|
||||||
echo "Swarm initialized"
|
|
||||||
|
|
||||||
# Create network
|
get_ip() {
|
||||||
docker network rm -f dokploy-network 2>/dev/null
|
# Try to get IPv4
|
||||||
docker network create --driver overlay --attachable dokploy-network
|
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
|
||||||
echo "Network created"
|
|
||||||
|
|
||||||
# Prepare configuration directory
|
if [ -n "$ipv4" ]; then
|
||||||
mkdir -p /etc/dokploy
|
echo "$ipv4"
|
||||||
chmod -R 777 /etc/dokploy
|
else
|
||||||
|
# Try to get IPv6
|
||||||
|
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
|
||||||
|
if [ -n "$ipv6" ]; then
|
||||||
|
echo "$ipv6"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}"
|
||||||
|
|
||||||
|
docker swarm init --advertise-addr $advertise_addr
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error: Failed to initialize Docker Swarm" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker network rm -f dokploy-network 2>/dev/null
|
||||||
|
docker network create --driver overlay --attachable dokploy-network
|
||||||
|
|
||||||
|
echo "Network created"
|
||||||
|
|
||||||
|
mkdir -p /etc/dokploy
|
||||||
|
|
||||||
|
chmod 777 /etc/dokploy
|
||||||
|
|
||||||
# Pull and deploy Dokploy
|
# Pull and deploy Dokploy
|
||||||
docker pull dokploy/dokploy:latest
|
docker pull dokploy/dokploy:latest
|
||||||
@@ -84,6 +105,7 @@ docker service create \
|
|||||||
-e PORT=<Value For PORT eg(3000)> \
|
-e PORT=<Value For PORT eg(3000)> \
|
||||||
-e TRAEFIK_SSL_PORT=<Value For SSL PORT eg(444)> \
|
-e TRAEFIK_SSL_PORT=<Value For SSL PORT eg(444)> \
|
||||||
-e TRAEFIK_PORT=<VALUE FOR TRAEFIK HTTP PORT eg(81)> \
|
-e TRAEFIK_PORT=<VALUE FOR TRAEFIK HTTP PORT eg(81)> \
|
||||||
|
-e ADVERTISE_ADDR=$advertise_addr \
|
||||||
dokploy/dokploy:latest
|
dokploy/dokploy:latest
|
||||||
|
|
||||||
# Output success message
|
# Output success message
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ The following templates are available:
|
|||||||
- **Wordpress**: Open Source Content Management System
|
- **Wordpress**: Open Source Content Management System
|
||||||
- **Open WebUI**: Free and Open Source ChatGPT Alternative
|
- **Open WebUI**: Free and Open Source ChatGPT Alternative
|
||||||
- **Teable**: Open Source Airtable Alternative, Developer Friendly, No-code Database Built on Postgres
|
- **Teable**: Open Source Airtable Alternative, Developer Friendly, No-code Database Built on Postgres
|
||||||
|
- **Roundcube**: Free and open source webmail software for the masses, written in PHP, uses SMTP[^1].
|
||||||
|
|
||||||
|
|
||||||
## Create your own template
|
## Create your own template
|
||||||
|
|
||||||
@@ -41,3 +40,5 @@ We accept contributions to upload new templates to the dokploy repository.
|
|||||||
Make sure to follow the guidelines for creating a template:
|
Make sure to follow the guidelines for creating a template:
|
||||||
|
|
||||||
[Steps to create your own template](https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#templates)
|
[Steps to create your own template](https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#templates)
|
||||||
|
|
||||||
|
[^1]: Please note that if you're self-hosting a mail server you need port 25 to be open for SMTP (Mail Transmission Protocol that allows you to send and receive) to work properly. Some VPS providers like [Hetzner](https://docs.hetzner.com/cloud/servers/faq/#why-can-i-not-send-any-mails-from-my-server) block this port by default for new clients.
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
|
|||||||
p: ({ children }) => (
|
p: ({ children }) => (
|
||||||
<p className="text-[#3E4342] dark:text-muted-foreground">{children}</p>
|
<p className="text-[#3E4342] dark:text-muted-foreground">{children}</p>
|
||||||
),
|
),
|
||||||
li: ({ children }) => (
|
li: ({ children, id }) => (
|
||||||
<li className="text-[#3E4342] dark:text-muted-foreground">{children}</li>
|
<li {...{ id }} className="text-[#3E4342] dark:text-muted-foreground">
|
||||||
|
{children}
|
||||||
|
</li>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,15 +21,11 @@
|
|||||||
"react-ga4": "^2.1.0"
|
"react-ga4": "^2.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tsx": "4.15.7",
|
"autoprefixer": "10.4.12",
|
||||||
"@biomejs/biome": "1.8.1",
|
|
||||||
"@types/mdx": "^2.0.13",
|
"@types/mdx": "^2.0.13",
|
||||||
"@types/node": "^20.14.2",
|
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
|
||||||
"autoprefixer": "^10.4.19",
|
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"tailwindcss": "^3.4.4",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5.4.5"
|
"typescript": "^5.4.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { addSuffixToAllProperties } from "@/server/utils/docker/compose";
|
import { addSuffixToAllProperties } from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import { addSuffixToConfigsRoot } from "@/server/utils/docker/compose/configs";
|
import { addSuffixToConfigsRoot } from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import { addSuffixToConfigsInServices } from "@/server/utils/docker/compose/configs";
|
import { addSuffixToConfigsInServices } from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import {
|
import { addSuffixToAllConfigs, addSuffixToConfigsRoot } from "@dokploy/server";
|
||||||
addSuffixToAllConfigs,
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
addSuffixToConfigsRoot,
|
|
||||||
} from "@/server/utils/docker/compose/configs";
|
|
||||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Domain } from "@/server/api/services/domain";
|
import type { Domain } from "@dokploy/server";
|
||||||
import { createDomainLabels } from "@/server/utils/docker/domain";
|
import { createDomainLabels } from "@dokploy/server";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
describe("createDomainLabels", () => {
|
describe("createDomainLabels", () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { addDokployNetworkToRoot } from "@/server/utils/docker/domain";
|
import { addDokployNetworkToRoot } from "@dokploy/server";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
describe("addDokployNetworkToRoot", () => {
|
describe("addDokployNetworkToRoot", () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { addDokployNetworkToService } from "@/server/utils/docker/domain";
|
import { addDokployNetworkToService } from "@dokploy/server";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
describe("addDokployNetworkToService", () => {
|
describe("addDokployNetworkToService", () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import { addSuffixToNetworksRoot } from "@/server/utils/docker/compose/network";
|
import { addSuffixToNetworksRoot } from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import { addSuffixToServiceNetworks } from "@/server/utils/docker/compose/network";
|
import { addSuffixToServiceNetworks } from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import {
|
import {
|
||||||
addSuffixToAllNetworks,
|
addSuffixToAllNetworks,
|
||||||
addSuffixToServiceNetworks,
|
addSuffixToServiceNetworks,
|
||||||
} from "@/server/utils/docker/compose/network";
|
} from "@dokploy/server";
|
||||||
import { addSuffixToNetworksRoot } from "@/server/utils/docker/compose/network";
|
import { addSuffixToNetworksRoot } from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import { addSuffixToSecretsRoot } from "@/server/utils/docker/compose/secrets";
|
import { addSuffixToSecretsRoot } from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { dump, load } from "js-yaml";
|
import { dump, load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import { addSuffixToSecretsInServices } from "@/server/utils/docker/compose/secrets";
|
import { addSuffixToSecretsInServices } from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { addSuffixToAllSecrets } from "@/server/utils/docker/compose/secrets";
|
import { addSuffixToAllSecrets } from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
addSuffixToAllServiceNames,
|
addSuffixToAllServiceNames,
|
||||||
addSuffixToServiceNames,
|
addSuffixToServiceNames,
|
||||||
} from "@/server/utils/docker/compose/service";
|
} from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import {
|
import { addSuffixToAllVolumes, addSuffixToVolumesRoot } from "@dokploy/server";
|
||||||
addSuffixToAllVolumes,
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
addSuffixToVolumesRoot,
|
|
||||||
} from "@/server/utils/docker/compose/volume";
|
|
||||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import { addSuffixToVolumesRoot } from "@/server/utils/docker/compose/volume";
|
import { addSuffixToVolumesRoot } from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import { addSuffixToVolumesInServices } from "@/server/utils/docker/compose/volume";
|
import { addSuffixToVolumesInServices } from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import {
|
import {
|
||||||
addSuffixToAllVolumes,
|
addSuffixToAllVolumes,
|
||||||
addSuffixToVolumesInServices,
|
addSuffixToVolumesInServices,
|
||||||
} from "@/server/utils/docker/compose/volume";
|
} from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { paths } from "@/server/constants";
|
import { paths } from "@dokploy/server/constants";
|
||||||
const { APPLICATIONS_PATH } = paths();
|
const { APPLICATIONS_PATH } = paths();
|
||||||
import type { ApplicationNested } from "@/server/utils/builders";
|
import type { ApplicationNested } from "@dokploy/server";
|
||||||
import { unzipDrop } from "@/server/utils/builders/drop";
|
import { unzipDrop } from "@dokploy/server";
|
||||||
import AdmZip from "adm-zip";
|
import AdmZip from "adm-zip";
|
||||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
vi.mock("@dokploy/server/constants", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal();
|
||||||
|
return {
|
||||||
|
// @ts-ignore
|
||||||
|
...actual,
|
||||||
|
paths: () => ({
|
||||||
|
APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
const undici = require("undici");
|
const undici = require("undici");
|
||||||
globalThis.File = undici.File as any;
|
globalThis.File = undici.File as any;
|
||||||
@@ -81,13 +92,6 @@ const baseApp: ApplicationNested = {
|
|||||||
username: null,
|
username: null,
|
||||||
dockerContextPath: null,
|
dockerContextPath: null,
|
||||||
};
|
};
|
||||||
//
|
|
||||||
vi.mock("@/server/constants", () => ({
|
|
||||||
paths: () => ({
|
|
||||||
APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
|
||||||
}),
|
|
||||||
// APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("unzipDrop using real zip files", () => {
|
describe("unzipDrop using real zip files", () => {
|
||||||
// const { APPLICATIONS_PATH } = paths();
|
// const { APPLICATIONS_PATH } = paths();
|
||||||
@@ -102,15 +106,19 @@ describe("unzipDrop using real zip files", () => {
|
|||||||
it("should correctly extract a zip with a single root folder", async () => {
|
it("should correctly extract a zip with a single root folder", async () => {
|
||||||
baseApp.appName = "single-file";
|
baseApp.appName = "single-file";
|
||||||
// const appName = "single-file";
|
// const appName = "single-file";
|
||||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
try {
|
||||||
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
|
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||||
|
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
|
||||||
const zipBuffer = zip.toBuffer();
|
console.log(`Output Path: ${outputPath}`);
|
||||||
const file = new File([zipBuffer], "single.zip");
|
const zipBuffer = zip.toBuffer();
|
||||||
await unzipDrop(file, baseApp);
|
const file = new File([zipBuffer], "single.zip");
|
||||||
|
await unzipDrop(file, baseApp);
|
||||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
expect(files.some((f) => f.name === "test.txt")).toBe(true);
|
expect(files.some((f) => f.name === "test.txt")).toBe(true);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
} finally {
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should correctly extract a zip with a single root folder and a subfolder", async () => {
|
it("should correctly extract a zip with a single root folder and a subfolder", async () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { parseRawConfig, processLogs } from "@/server/utils/access-log/utils";
|
import { parseRawConfig, processLogs } from "@dokploy/server";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`;
|
const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`;
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ vi.mock("node:fs", () => ({
|
|||||||
default: fs,
|
default: fs,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import type { Admin } from "@/server/api/services/admin";
|
import type { Admin, FileConfig } from "@dokploy/server";
|
||||||
import { createDefaultServerTraefikConfig } from "@/server/setup/traefik-setup";
|
import {
|
||||||
import { loadOrCreateConfig } from "@/server/utils/traefik/application";
|
createDefaultServerTraefikConfig,
|
||||||
import type { FileConfig } from "@/server/utils/traefik/file-types";
|
loadOrCreateConfig,
|
||||||
import { updateServerTraefik } from "@/server/utils/traefik/web-server";
|
updateServerTraefik,
|
||||||
|
} from "@dokploy/server";
|
||||||
import { beforeEach, expect, test, vi } from "vitest";
|
import { beforeEach, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
const baseAdmin: Admin = {
|
const baseAdmin: Admin = {
|
||||||
@@ -23,6 +24,9 @@ const baseAdmin: Admin = {
|
|||||||
sshPrivateKey: null,
|
sshPrivateKey: null,
|
||||||
enableDockerCleanup: false,
|
enableDockerCleanup: false,
|
||||||
enableLogRotation: false,
|
enableLogRotation: false,
|
||||||
|
serversQuantity: 0,
|
||||||
|
stripeCustomerId: "",
|
||||||
|
stripeSubscriptionId: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Domain } from "@/server/api/services/domain";
|
import type { Domain } from "@dokploy/server";
|
||||||
import type { Redirect } from "@/server/api/services/redirect";
|
import type { Redirect } from "@dokploy/server";
|
||||||
import type { ApplicationNested } from "@/server/utils/builders";
|
import type { ApplicationNested } from "@dokploy/server";
|
||||||
import { createRouterConfig } from "@/server/utils/traefik/domain";
|
import { createRouterConfig } from "@dokploy/server";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
const baseApp: ApplicationNested = {
|
const baseApp: ApplicationNested = {
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
|
import path from "node:path";
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
|
||||||
tsconfigPaths({
|
|
||||||
root: "./",
|
|
||||||
projects: ["tsconfig.json"],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
test: {
|
test: {
|
||||||
include: ["__test__/**/*.test.ts"], // Incluir solo los archivos de test en el directorio __test__
|
include: ["__test__/**/*.test.ts"], // Incluir solo los archivos de test en el directorio __test__
|
||||||
exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"],
|
exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"],
|
||||||
pool: "forks",
|
pool: "forks",
|
||||||
},
|
},
|
||||||
|
define: {
|
||||||
|
"process.env": {
|
||||||
|
NODE: "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@dokploy/server": path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../../../packages/server/src",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input, NumberInput } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -125,28 +125,14 @@ export const UpdatePort = ({ portId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Published Port</FormLabel>
|
<FormLabel>Published Port</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<NumberInput placeholder="1-65535" {...field} />
|
||||||
placeholder="1-65535"
|
|
||||||
{...field}
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(0);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
field.onChange(number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="targetPort"
|
name="targetPort"
|
||||||
@@ -154,22 +140,7 @@ export const UpdatePort = ({ portId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Target Port</FormLabel>
|
<FormLabel>Target Port</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input placeholder="1-65535" {...field} />
|
||||||
placeholder="1-65535"
|
|
||||||
{...field}
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(0);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
field.onChange(number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -19,6 +19,15 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Switch } from "@/components/ui/switch";
|
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";
|
||||||
@@ -36,6 +45,36 @@ const AddRedirectchema = z.object({
|
|||||||
|
|
||||||
type AddRedirect = z.infer<typeof AddRedirectchema>;
|
type AddRedirect = z.infer<typeof AddRedirectchema>;
|
||||||
|
|
||||||
|
// Default presets
|
||||||
|
const redirectPresets = [
|
||||||
|
// {
|
||||||
|
// label: "Allow www & non-www.",
|
||||||
|
// redirect: {
|
||||||
|
// regex: "",
|
||||||
|
// permanent: false,
|
||||||
|
// replacement: "",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
id: "to-www",
|
||||||
|
label: "Redirect to www",
|
||||||
|
redirect: {
|
||||||
|
regex: "^https?://(?:www.)?(.+)",
|
||||||
|
permanent: true,
|
||||||
|
replacement: "https://www.$${1}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "to-non-www",
|
||||||
|
label: "Redirect to non-www",
|
||||||
|
redirect: {
|
||||||
|
regex: "^https?://www.(.+)",
|
||||||
|
permanent: true,
|
||||||
|
replacement: "https://$${1}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
@@ -43,9 +82,10 @@ interface Props {
|
|||||||
|
|
||||||
export const AddRedirect = ({
|
export const AddRedirect = ({
|
||||||
applicationId,
|
applicationId,
|
||||||
children = <PlusIcon className="h-4 w-4" />,
|
children = <PlusIcon className="w-4 h-4" />,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [presetSelected, setPresetSelected] = useState("");
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
@@ -81,19 +121,36 @@ export const AddRedirect = ({
|
|||||||
await utils.application.readTraefikConfig.invalidate({
|
await utils.application.readTraefikConfig.invalidate({
|
||||||
applicationId,
|
applicationId,
|
||||||
});
|
});
|
||||||
setIsOpen(false);
|
onDialogToggle(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to create the redirect");
|
toast.error("Error to create the redirect");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onDialogToggle = (open: boolean) => {
|
||||||
|
setIsOpen(open);
|
||||||
|
// commented for the moment because not reseting the form if accidentally closed the dialog can be considered as a feature instead of a bug
|
||||||
|
// setPresetSelected("");
|
||||||
|
// form.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPresetSelect = (presetId: string) => {
|
||||||
|
const redirectPreset = redirectPresets.find(
|
||||||
|
(preset) => preset.id === presetId,
|
||||||
|
)?.redirect;
|
||||||
|
if (!redirectPreset) return;
|
||||||
|
const { regex, permanent, replacement } = redirectPreset;
|
||||||
|
form.reset({ regex, permanent, replacement }, { keepDefaultValues: true });
|
||||||
|
setPresetSelected(presetId);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={onDialogToggle}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button>{children}</Button>
|
<Button>{children}</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Redirects</DialogTitle>
|
<DialogTitle>Redirects</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -102,6 +159,24 @@ export const AddRedirect = ({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<Label>Presets</Label>
|
||||||
|
<Select onValueChange={onPresetSelect} value={presetSelected}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="No preset selected" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{redirectPresets.map((preset) => (
|
||||||
|
<SelectItem key={preset.label} value={preset.id}>
|
||||||
|
{preset.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
id="hook-form-add-redirect"
|
id="hook-form-add-redirect"
|
||||||
@@ -142,7 +217,7 @@ export const AddRedirect = ({
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="permanent"
|
name="permanent"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel>Permanent</FormLabel>
|
<FormLabel>Permanent</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Resources</CardTitle>
|
<CardTitle className="text-xl">Resources</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
If you want to decrease or increase the resources to a specific
|
If you want to decrease or increase the resources to a specific.
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -16,20 +16,37 @@ interface Props {
|
|||||||
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
||||||
const [data, setData] = useState("");
|
const [data, setData] = useState("");
|
||||||
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
||||||
|
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !logPath) return;
|
if (!open || !logPath) return;
|
||||||
|
|
||||||
|
setData("");
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
|
||||||
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}${serverId ? `&serverId=${serverId}` : ""}`;
|
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}${serverId ? `&serverId=${serverId}` : ""}`;
|
||||||
const ws = new WebSocket(wsUrl);
|
const ws = new WebSocket(wsUrl);
|
||||||
|
wsRef.current = ws; // Store WebSocket instance in ref
|
||||||
|
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
setData((currentData) => currentData + e.data);
|
setData((currentData) => currentData + e.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
return () => ws.close();
|
ws.onerror = (error) => {
|
||||||
|
console.error("WebSocket error: ", error);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log("WebSocket connection closed");
|
||||||
|
wsRef.current = null; // Clear reference on close
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||||
|
ws.close();
|
||||||
|
wsRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
}, [logPath, open]);
|
}, [logPath, open]);
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
@@ -45,7 +62,15 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
|||||||
open={open}
|
open={open}
|
||||||
onOpenChange={(e) => {
|
onOpenChange={(e) => {
|
||||||
onClose();
|
onClose();
|
||||||
if (!e) setData("");
|
if (!e) {
|
||||||
|
setData("");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wsRef.current) {
|
||||||
|
if (wsRef.current.readyState === WebSocket.OPEN) {
|
||||||
|
wsRef.current.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input, NumberInput } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -140,7 +140,7 @@ export const AddDomain = ({
|
|||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
{children}
|
{children}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Domain</DialogTitle>
|
<DialogTitle>Domain</DialogTitle>
|
||||||
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||||
@@ -228,19 +228,36 @@ export const AddDomain = ({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Container Port</FormLabel>
|
<FormLabel>Container Port</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<NumberInput placeholder={"3000"} {...field} />
|
||||||
placeholder={"3000"}
|
|
||||||
{...field}
|
|
||||||
onChange={(e) => {
|
|
||||||
field.onChange(Number.parseInt(e.target.value));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="https"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>HTTPS</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Automatically provision SSL Certificate.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
{form.getValues().https && (
|
{form.getValues().https && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -270,28 +287,6 @@ export const AddDomain = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="https"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>HTTPS</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Automatically provision SSL Certificate.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
<div className="flex w-full flex-col items-center justify-center gap-3">
|
<div className="flex w-full flex-col items-center justify-center gap-3">
|
||||||
<GlobeIcon className="size-8 text-muted-foreground" />
|
<GlobeIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To access to the application is required to set at least 1
|
To access the application it is required to set at least 1
|
||||||
domain
|
domain
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
|
|||||||
@@ -21,20 +21,38 @@ export const ShowDeploymentCompose = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [data, setData] = useState("");
|
const [data, setData] = useState("");
|
||||||
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
||||||
|
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !logPath) return;
|
if (!open || !logPath) return;
|
||||||
|
|
||||||
|
setData("");
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
|
||||||
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}&serverId=${serverId}`;
|
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}&serverId=${serverId}`;
|
||||||
const ws = new WebSocket(wsUrl);
|
const ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
wsRef.current = ws; // Store WebSocket instance in ref
|
||||||
|
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
setData((currentData) => currentData + e.data);
|
setData((currentData) => currentData + e.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
return () => ws.close();
|
ws.onerror = (error) => {
|
||||||
|
console.error("WebSocket error: ", error);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log("WebSocket connection closed");
|
||||||
|
wsRef.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||||
|
ws.close();
|
||||||
|
wsRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
}, [logPath, open]);
|
}, [logPath, open]);
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
@@ -50,7 +68,15 @@ export const ShowDeploymentCompose = ({
|
|||||||
open={open}
|
open={open}
|
||||||
onOpenChange={(e) => {
|
onOpenChange={(e) => {
|
||||||
onClose();
|
onClose();
|
||||||
if (!e) setData("");
|
if (!e) {
|
||||||
|
setData("");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wsRef.current) {
|
||||||
|
if (wsRef.current.readyState === WebSocket.OPEN) {
|
||||||
|
wsRef.current.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input, NumberInput } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -161,7 +161,7 @@ export const AddDomainCompose = ({
|
|||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
{children}
|
{children}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Domain</DialogTitle>
|
<DialogTitle>Domain</DialogTitle>
|
||||||
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||||
@@ -190,7 +190,7 @@ export const AddDomainCompose = ({
|
|||||||
{errorServices?.message}
|
{errorServices?.message}
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-row gap-4 w-full items-end">
|
<div className="flex flex-row items-end w-full gap-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="serviceName"
|
name="serviceName"
|
||||||
@@ -364,19 +364,36 @@ export const AddDomainCompose = ({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Container Port</FormLabel>
|
<FormLabel>Container Port</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<NumberInput placeholder={"3000"} {...field} />
|
||||||
placeholder={"3000"}
|
|
||||||
{...field}
|
|
||||||
onChange={(e) => {
|
|
||||||
field.onChange(Number.parseInt(e.target.value));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="https"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>HTTPS</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Automatically provision SSL Certificate.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
{https && (
|
{https && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -406,28 +423,6 @@ export const AddDomainCompose = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="https"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>HTTPS</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Automatically provision SSL Certificate.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const ShowDomainsCompose = ({ composeId }: Props) => {
|
|||||||
<div className="flex w-full flex-col items-center justify-center gap-3">
|
<div className="flex w-full flex-col items-center justify-center gap-3">
|
||||||
<GlobeIcon className="size-8 text-muted-foreground" />
|
<GlobeIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To access to the application is required to set at least 1
|
To access to the application it is required to set at least 1
|
||||||
domain
|
domain
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.log(e);
|
|
||||||
toast.error("Error to update the compose config");
|
toast.error("Error to update the compose config");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Puzzle, RefreshCw } from "lucide-react";
|
import { Puzzle, RefreshCw } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -34,6 +34,16 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
|
|||||||
|
|
||||||
const { mutateAsync, isLoading } = api.compose.fetchSourceType.useMutation();
|
const { mutateAsync, isLoading } = api.compose.fetchSourceType.useMutation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
mutateAsync({ composeId })
|
||||||
|
.then(() => {
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch((err) => {});
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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 { Terminal } from "@xterm/xterm";
|
import { Terminal } from "@xterm/xterm";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { FitAddon } from "xterm-addon-fit";
|
import { FitAddon } from "xterm-addon-fit";
|
||||||
import "@xterm/xterm/css/xterm.css";
|
import "@xterm/xterm/css/xterm.css";
|
||||||
|
|
||||||
@@ -18,12 +18,24 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [term, setTerm] = React.useState<Terminal>();
|
const [term, setTerm] = React.useState<Terminal>();
|
||||||
const [lines, setLines] = React.useState<number>(40);
|
const [lines, setLines] = React.useState<number>(40);
|
||||||
|
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
||||||
|
|
||||||
const createTerminal = (): Terminal => {
|
useEffect(() => {
|
||||||
|
// if (containerId === "select-a-container") {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
const container = document.getElementById(id);
|
const container = document.getElementById(id);
|
||||||
if (container) {
|
if (container) {
|
||||||
container.innerHTML = "";
|
container.innerHTML = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (wsRef.current) {
|
||||||
|
console.log(wsRef.current);
|
||||||
|
if (wsRef.current.readyState === WebSocket.OPEN) {
|
||||||
|
wsRef.current.close();
|
||||||
|
}
|
||||||
|
wsRef.current = null;
|
||||||
|
}
|
||||||
const termi = new Terminal({
|
const termi = new Terminal({
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
cols: 80,
|
cols: 80,
|
||||||
@@ -45,7 +57,7 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
|
|
||||||
const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}${serverId ? `&serverId=${serverId}` : ""}`;
|
const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}${serverId ? `&serverId=${serverId}` : ""}`;
|
||||||
const ws = new WebSocket(wsUrl);
|
const ws = new WebSocket(wsUrl);
|
||||||
|
wsRef.current = ws;
|
||||||
const fitAddon = new FitAddon();
|
const fitAddon = new FitAddon();
|
||||||
termi.loadAddon(fitAddon);
|
termi.loadAddon(fitAddon);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -54,6 +66,10 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
termi.focus();
|
termi.focus();
|
||||||
setTerm(termi);
|
setTerm(termi);
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error("WebSocket error: ", error);
|
||||||
|
};
|
||||||
|
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
termi.write(e.data);
|
termi.write(e.data);
|
||||||
};
|
};
|
||||||
@@ -62,12 +78,14 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
console.log(e.reason);
|
console.log(e.reason);
|
||||||
|
|
||||||
termi.write(`Connection closed!\nReason: ${e.reason}\n`);
|
termi.write(`Connection closed!\nReason: ${e.reason}\n`);
|
||||||
|
wsRef.current = null;
|
||||||
|
};
|
||||||
|
return () => {
|
||||||
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||||
|
ws.close();
|
||||||
|
wsRef.current = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
return termi;
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
createTerminal();
|
|
||||||
}, [lines, containerId]);
|
}, [lines, containerId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const ShowMariadbResources = ({ mariadbId }: Props) => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Resources</CardTitle>
|
<CardTitle className="text-xl">Resources</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
If you want to decrease or increase the resources to a specific
|
If you want to decrease or increase the resources to a specific.
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const ShowBackupMariadb = ({ mariadbId }: Props) => {
|
|||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<CardTitle className="text-xl">Backups</CardTitle>
|
<CardTitle className="text-xl">Backups</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Add backup to your database to save the data to a different
|
Add backups to your database to save the data to a different
|
||||||
providers.
|
providers.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,8 +62,8 @@ export const ShowBackupMariadb = ({ mariadbId }: Props) => {
|
|||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<DatabaseBackup className="size-8 text-muted-foreground" />
|
<DatabaseBackup className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To create a backup is required to set at least 1 provider. Please,
|
To create a backup it is required to set at least 1 provider.
|
||||||
go to{" "}
|
Please, go to{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/settings/server"
|
href="/dashboard/settings/server"
|
||||||
className="text-foreground"
|
className="text-foreground"
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
|||||||
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
|
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
|
||||||
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
@@ -79,7 +80,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
|||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`;
|
return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
@@ -90,7 +91,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
|||||||
form,
|
form,
|
||||||
data?.databaseName,
|
data?.databaseName,
|
||||||
data?.databaseUser,
|
data?.databaseUser,
|
||||||
ip,
|
getIp,
|
||||||
]);
|
]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Resources</CardTitle>
|
<CardTitle className="text-xl">Resources</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
If you want to decrease or increase the resources to a specific
|
If you want to decrease or increase the resources to a specific.
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ export const ShowBackupMongo = ({ mongoId }: Props) => {
|
|||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<CardTitle className="text-xl">Backups</CardTitle>
|
<CardTitle className="text-xl">Backups</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Add backup to your database to save the data to a different
|
Add backups to your database to save the data to a different
|
||||||
providers.
|
provider.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -62,8 +62,8 @@ export const ShowBackupMongo = ({ mongoId }: Props) => {
|
|||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<DatabaseBackup className="size-8 text-muted-foreground" />
|
<DatabaseBackup className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To create a backup is required to set at least 1 provider. Please,
|
To create a backup it is required to set at least 1 provider.
|
||||||
go to{" "}
|
Please, go to{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/settings/server"
|
href="/dashboard/settings/server"
|
||||||
className="text-foreground"
|
className="text-foreground"
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
|||||||
const { data, refetch } = api.mongo.one.useQuery({ mongoId });
|
const { data, refetch } = api.mongo.one.useQuery({ mongoId });
|
||||||
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
@@ -80,7 +80,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
|||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}`;
|
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
@@ -90,7 +90,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
|||||||
data?.databasePassword,
|
data?.databasePassword,
|
||||||
form,
|
form,
|
||||||
data?.databaseUser,
|
data?.databaseUser,
|
||||||
ip,
|
getIp,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const ShowVolumes = ({ mongoId }: Props) => {
|
|||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-xl">Volumes</CardTitle>
|
<CardTitle className="text-xl">Volumes</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
If you want to persist data in this mongo use the following config
|
If you want to persist data in this mongo use the following config.
|
||||||
to setup the volumes
|
to setup the volumes
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ export const DockerMonitoring = ({
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Monitoring</CardTitle>
|
<CardTitle className="text-xl">Monitoring</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Watch the usage of your server in the current app
|
Watch the usage of your server in the current app.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Resources</CardTitle>
|
<CardTitle className="text-xl">Resources</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
If you want to decrease or increase the resources to a specific
|
If you want to decrease or increase the resources to a specific.
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ export const ShowBackupMySql = ({ mysqlId }: Props) => {
|
|||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<CardTitle className="text-xl">Backups</CardTitle>
|
<CardTitle className="text-xl">Backups</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Add backup to your database to save the data to a different
|
Add backups to your database to save the data to a different
|
||||||
providers.
|
provider.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -62,8 +62,8 @@ export const ShowBackupMySql = ({ mysqlId }: Props) => {
|
|||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<DatabaseBackup className="size-8 text-muted-foreground" />
|
<DatabaseBackup className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To create a backup is required to set at least 1 provider. Please,
|
To create a backup it is required to set at least 1 provider.
|
||||||
go to{" "}
|
Please, go to{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/settings/server"
|
href="/dashboard/settings/server"
|
||||||
className="text-foreground"
|
className="text-foreground"
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
|||||||
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
|
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
|
||||||
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
@@ -80,7 +80,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
|||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`;
|
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
@@ -91,7 +91,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
|||||||
data?.databaseName,
|
data?.databaseName,
|
||||||
data?.databaseUser,
|
data?.databaseUser,
|
||||||
form,
|
form,
|
||||||
ip,
|
getIp,
|
||||||
]);
|
]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const ShowPostgresResources = ({ postgresId }: Props) => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Resources</CardTitle>
|
<CardTitle className="text-xl">Resources</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
If you want to decrease or increase the resources to a specific
|
If you want to decrease or increase the resources to a specific.
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => {
|
|||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<CardTitle className="text-xl">Backups</CardTitle>
|
<CardTitle className="text-xl">Backups</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Add backup to your database to save the data to a different
|
Add backups to your database to save the data to a different
|
||||||
providers.
|
provider.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -63,8 +63,8 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => {
|
|||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<DatabaseBackup className="size-8 text-muted-foreground" />
|
<DatabaseBackup className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To create a backup is required to set at least 1 provider. Please,
|
To create a backup it is required to set at least 1 provider.
|
||||||
go to{" "}
|
Please, go to{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/settings/server"
|
href="/dashboard/settings/server"
|
||||||
className="text-foreground"
|
className="text-foreground"
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
|||||||
const { data, refetch } = api.postgres.one.useQuery({ postgresId });
|
const { data, refetch } = api.postgres.one.useQuery({ postgresId });
|
||||||
const { mutateAsync, isLoading } =
|
const { mutateAsync, isLoading } =
|
||||||
api.postgres.saveExternalPort.useMutation();
|
api.postgres.saveExternalPort.useMutation();
|
||||||
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
|
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
@@ -79,10 +80,9 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const hostname = window.location.hostname;
|
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`;
|
return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
@@ -92,7 +92,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
|||||||
data?.databasePassword,
|
data?.databasePassword,
|
||||||
form,
|
form,
|
||||||
data?.databaseName,
|
data?.databaseName,
|
||||||
ip,
|
getIp,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import { slugify } from "@/lib/slug";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CircuitBoard, HelpCircle } from "lucide-react";
|
import { CircuitBoard, HelpCircle } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -71,6 +71,7 @@ interface Props {
|
|||||||
|
|
||||||
export const AddCompose = ({ projectId, projectName }: Props) => {
|
export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
const slug = slugify(projectName);
|
const slug = slugify(projectName);
|
||||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
@@ -101,6 +102,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
|||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Compose Created");
|
toast.success("Compose Created");
|
||||||
|
setVisible(false);
|
||||||
await utils.project.one.invalidate({
|
await utils.project.one.invalidate({
|
||||||
projectId,
|
projectId,
|
||||||
});
|
});
|
||||||
@@ -111,7 +113,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={visible} onOpenChange={setVisible}>
|
||||||
<DialogTrigger className="w-full">
|
<DialogTrigger className="w-full">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="w-full cursor-pointer space-x-3"
|
className="w-full cursor-pointer space-x-3"
|
||||||
|
|||||||
@@ -15,20 +15,25 @@ import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
BookIcon,
|
BookIcon,
|
||||||
|
CircuitBoard,
|
||||||
|
ExternalLink,
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
FolderInput,
|
FolderInput,
|
||||||
MoreHorizontalIcon,
|
MoreHorizontalIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { Fragment } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { UpdateProject } from "./update";
|
import { UpdateProject } from "./update";
|
||||||
|
|
||||||
@@ -45,6 +50,7 @@ export const ShowProjects = () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
const { mutateAsync } = api.project.remove.useMutation();
|
const { mutateAsync } = api.project.remove.useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data?.length === 0 && (
|
{data?.length === 0 && (
|
||||||
@@ -74,17 +80,87 @@ export const ShowProjects = () => {
|
|||||||
project?.redis.length +
|
project?.redis.length +
|
||||||
project?.applications.length +
|
project?.applications.length +
|
||||||
project?.compose.length;
|
project?.compose.length;
|
||||||
|
|
||||||
|
const flattedDomains = [
|
||||||
|
...project.applications.flatMap((a) => a.domains),
|
||||||
|
...project.compose.flatMap((a) => a.domains),
|
||||||
|
];
|
||||||
|
|
||||||
|
const renderDomainsDropdown = (
|
||||||
|
item: typeof project.compose | typeof project.applications,
|
||||||
|
) =>
|
||||||
|
item[0] ? (
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuLabel>
|
||||||
|
{"applicationId" in item[0] ? "Applications" : "Compose"}
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
{item.map((a) => (
|
||||||
|
<Fragment
|
||||||
|
key={"applicationId" in a ? a.applicationId : a.composeId}
|
||||||
|
>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuLabel className="font-normal capitalize text-xs ">
|
||||||
|
{a.name}
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
{a.domains.map((domain) => (
|
||||||
|
<DropdownMenuItem key={domain.domainId} asChild>
|
||||||
|
<Link
|
||||||
|
className="space-x-4 text-xs cursor-pointer justify-between"
|
||||||
|
target="_blank"
|
||||||
|
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
||||||
|
>
|
||||||
|
<span>{domain.host}</span>
|
||||||
|
<ExternalLink className="size-4 shrink-0" />
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={project.projectId} className="w-full lg:max-w-md">
|
<div key={project.projectId} className="w-full lg:max-w-md">
|
||||||
<Link href={`/dashboard/project/${project.projectId}`}>
|
<Link href={`/dashboard/project/${project.projectId}`}>
|
||||||
<Card className="group relative w-full bg-transparent transition-colors hover:bg-card">
|
<Card className="group relative w-full bg-transparent transition-colors hover:bg-card">
|
||||||
<Button
|
{flattedDomains.length > 1 ? (
|
||||||
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
|
<DropdownMenu>
|
||||||
size="sm"
|
<DropdownMenuTrigger asChild>
|
||||||
variant="default"
|
<Button
|
||||||
>
|
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
|
||||||
<ExternalLinkIcon className="size-3.5" />
|
size="sm"
|
||||||
</Button>
|
variant="default"
|
||||||
|
>
|
||||||
|
<ExternalLinkIcon className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
className="w-[200px] space-y-2"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{renderDomainsDropdown(project.applications)}
|
||||||
|
{renderDomainsDropdown(project.compose)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
) : flattedDomains[0] ? (
|
||||||
|
<Button
|
||||||
|
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
|
||||||
|
size="sm"
|
||||||
|
variant="default"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={`${flattedDomains[0].https ? "https" : "http"}://${flattedDomains[0].host}${flattedDomains[0].path}`}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<ExternalLinkIcon className="size-3.5" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center justify-between gap-2">
|
<CardTitle className="flex items-center justify-between gap-2">
|
||||||
<span className="flex flex-col gap-1.5">
|
<span className="flex flex-col gap-1.5">
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const ShowRedisResources = ({ redisId }: Props) => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Resources</CardTitle>
|
<CardTitle className="text-xl">Resources</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
If you want to decrease or increase the resources to a specific
|
If you want to decrease or increase the resources to a specific.
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
|||||||
const { data, refetch } = api.redis.one.useQuery({ redisId });
|
const { data, refetch } = api.redis.one.useQuery({ redisId });
|
||||||
const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
|
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
@@ -81,11 +82,11 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
|||||||
const hostname = window.location.hostname;
|
const hostname = window.location.hostname;
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `redis://default:${data?.databasePassword}@${ip}:${port}`;
|
return `redis://default:${data?.databasePassword}@${getIp}:${port}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
}, [data?.appName, data?.externalPort, data?.databasePassword, form, ip]);
|
}, [data?.appName, data?.externalPort, data?.databasePassword, form, getIp]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { NumberInput } from "@/components/ui/input";
|
||||||
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { loadStripe } from "@stripe/stripe-js";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { AlertTriangle, CheckIcon, MinusIcon, PlusIcon } from "lucide-react";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
const stripePromise = loadStripe(
|
||||||
|
"pk_test_51QAm7bF3cxQuHeOz0xg04o9teeyTbbNHQPJ5Tr98MlTEan9MzewT3gwh0jSWBNvrRWZ5vASoBgxUSF4gPWsJwATk00Ir2JZ0S1",
|
||||||
|
);
|
||||||
|
|
||||||
|
export const calculatePrice = (count: number, isAnnual = false) => {
|
||||||
|
if (isAnnual) {
|
||||||
|
if (count <= 1) return 45.9;
|
||||||
|
return 35.7 * count;
|
||||||
|
}
|
||||||
|
if (count <= 1) return 4.5;
|
||||||
|
return count * 3.5;
|
||||||
|
};
|
||||||
|
// 178.156.147.118
|
||||||
|
export const ShowBilling = () => {
|
||||||
|
const { data: servers } = api.server.all.useQuery(undefined);
|
||||||
|
const { data: admin } = api.admin.one.useQuery();
|
||||||
|
const { data } = api.stripe.getProducts.useQuery();
|
||||||
|
const { mutateAsync: createCheckoutSession } =
|
||||||
|
api.stripe.createCheckoutSession.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: createCustomerPortalSession } =
|
||||||
|
api.stripe.createCustomerPortalSession.useMutation();
|
||||||
|
|
||||||
|
const [serverQuantity, setServerQuantity] = useState(3);
|
||||||
|
const [isAnnual, setIsAnnual] = useState(false);
|
||||||
|
|
||||||
|
const handleCheckout = async (productId: string) => {
|
||||||
|
const stripe = await stripePromise;
|
||||||
|
if (data && data.subscriptions.length === 0) {
|
||||||
|
createCheckoutSession({
|
||||||
|
productId,
|
||||||
|
serverQuantity: serverQuantity,
|
||||||
|
isAnnual,
|
||||||
|
}).then(async (session) => {
|
||||||
|
await stripe?.redirectToCheckout({
|
||||||
|
sessionId: session.sessionId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const products = data?.products.filter((product) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const interval = product?.default_price?.recurring?.interval;
|
||||||
|
return isAnnual ? interval === "year" : interval === "month";
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxServers = admin?.serversQuantity ?? 1;
|
||||||
|
const percentage = ((servers?.length ?? 0) / maxServers) * 100;
|
||||||
|
const safePercentage = Math.min(percentage, 100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 w-full">
|
||||||
|
<Tabs
|
||||||
|
defaultValue="monthly"
|
||||||
|
value={isAnnual ? "annual" : "monthly"}
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(e) => setIsAnnual(e === "annual")}
|
||||||
|
>
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="monthly">Monthly</TabsTrigger>
|
||||||
|
<TabsTrigger value="annual">Annual</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
{admin?.stripeSubscriptionId && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-lg font-medium">Servers Plan</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
You have {servers?.length} server on your plan of{" "}
|
||||||
|
{admin?.serversQuantity} servers
|
||||||
|
</p>
|
||||||
|
<div className="pb-5">
|
||||||
|
<Progress value={safePercentage} className="max-w-lg" />
|
||||||
|
</div>
|
||||||
|
{admin && (
|
||||||
|
<>
|
||||||
|
{admin.serversQuantity! <= servers?.length! && (
|
||||||
|
<div className="flex flex-row gap-4 p-2 bg-yellow-50 dark:bg-yellow-950 rounded-lg items-center">
|
||||||
|
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
|
||||||
|
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||||
|
You have reached the maximum number of servers you can
|
||||||
|
create, please upgrade your plan to add more servers.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{products?.map((product) => {
|
||||||
|
const featured = true;
|
||||||
|
return (
|
||||||
|
<div key={product.id}>
|
||||||
|
<section
|
||||||
|
className={clsx(
|
||||||
|
"flex flex-col rounded-3xl border-dashed border-2 px-4 max-w-sm",
|
||||||
|
featured
|
||||||
|
? "order-first bg-black border py-8 lg:order-none"
|
||||||
|
: "lg:py-8",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isAnnual ? (
|
||||||
|
<div className="flex flex-row gap-2 items-center">
|
||||||
|
<p className=" text-2xl font-semibold tracking-tight text-primary ">
|
||||||
|
$ {calculatePrice(serverQuantity, isAnnual).toFixed(2)} USD
|
||||||
|
</p>
|
||||||
|
|
|
||||||
|
<p className=" text-base font-semibold tracking-tight text-muted-foreground">
|
||||||
|
${" "}
|
||||||
|
{(calculatePrice(serverQuantity, isAnnual) / 12).toFixed(2)}{" "}
|
||||||
|
/ Month USD
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className=" text-2xl font-semibold tracking-tight text-primary ">
|
||||||
|
$ {calculatePrice(serverQuantity, isAnnual).toFixed(2)} USD
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<h3 className="mt-5 font-medium text-lg text-white">
|
||||||
|
{product.name}
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
className={clsx(
|
||||||
|
"text-sm",
|
||||||
|
featured ? "text-white" : "text-slate-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{product.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
role="list"
|
||||||
|
className={clsx(
|
||||||
|
" mt-4 flex flex-col gap-y-2 text-sm",
|
||||||
|
featured ? "text-white" : "text-slate-200",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
"All the features of Dokploy",
|
||||||
|
"Unlimited deployments",
|
||||||
|
"Self-hosted on your own infrastructure",
|
||||||
|
"Full access to all deployment features",
|
||||||
|
"Dokploy integration",
|
||||||
|
"Backups",
|
||||||
|
"All Incoming features",
|
||||||
|
].map((feature) => (
|
||||||
|
<li key={feature} className="flex text-muted-foreground">
|
||||||
|
<CheckIcon />
|
||||||
|
<span className="ml-4">{feature}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="flex flex-col gap-2 mt-4">
|
||||||
|
<div className="flex items-center gap-2 justify-center">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{serverQuantity} Servers
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
disabled={serverQuantity <= 1}
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
if (serverQuantity <= 1) return;
|
||||||
|
|
||||||
|
setServerQuantity(serverQuantity - 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MinusIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<NumberInput
|
||||||
|
value={serverQuantity}
|
||||||
|
onChange={(e) => {
|
||||||
|
setServerQuantity(e.target.value as unknown as number);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setServerQuantity(serverQuantity + 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
data?.subscriptions && data?.subscriptions?.length > 0
|
||||||
|
? "justify-between"
|
||||||
|
: "justify-end",
|
||||||
|
"flex flex-row items-center gap-2 mt-4",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{admin?.stripeCustomerId && (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className="w-full"
|
||||||
|
onClick={async () => {
|
||||||
|
const session = await createCustomerPortalSession();
|
||||||
|
|
||||||
|
window.open(session.url);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Manage Subscription
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data?.subscriptions?.length === 0 && (
|
||||||
|
<div className="justify-end w-full">
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
onClick={async () => {
|
||||||
|
handleCheckout(product.id);
|
||||||
|
}}
|
||||||
|
disabled={serverQuantity < 1}
|
||||||
|
>
|
||||||
|
Subscribe
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -18,10 +18,25 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle } from "lucide-react";
|
import { AlertTriangle, HelpCircle } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -35,6 +50,7 @@ const addCertificate = z.object({
|
|||||||
certificateData: z.string().min(1, "Certificate data is required"),
|
certificateData: z.string().min(1, "Certificate data is required"),
|
||||||
privateKey: z.string().min(1, "Private key is required"),
|
privateKey: z.string().min(1, "Private key is required"),
|
||||||
autoRenew: z.boolean().optional(),
|
autoRenew: z.boolean().optional(),
|
||||||
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddCertificate = z.infer<typeof addCertificate>;
|
type AddCertificate = z.infer<typeof addCertificate>;
|
||||||
@@ -44,6 +60,7 @@ export const AddCertificate = () => {
|
|||||||
|
|
||||||
const { mutateAsync, isError, error, isLoading } =
|
const { mutateAsync, isError, error, isLoading } =
|
||||||
api.certificates.create.useMutation();
|
api.certificates.create.useMutation();
|
||||||
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
|
|
||||||
const form = useForm<AddCertificate>({
|
const form = useForm<AddCertificate>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -64,6 +81,7 @@ export const AddCertificate = () => {
|
|||||||
certificateData: data.certificateData,
|
certificateData: data.certificateData,
|
||||||
privateKey: data.privateKey,
|
privateKey: data.privateKey,
|
||||||
autoRenew: data.autoRenew,
|
autoRenew: data.autoRenew,
|
||||||
|
serverId: data.serverId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Certificate Created");
|
toast.success("Certificate Created");
|
||||||
@@ -144,6 +162,47 @@ export const AddCertificate = () => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="serverId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
|
Select a Server (Optional)
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
|
</FormLabel>
|
||||||
|
</TooltipTrigger>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a Server" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{servers?.map((server) => (
|
||||||
|
<SelectItem
|
||||||
|
key={server.serverId}
|
||||||
|
value={server.serverId}
|
||||||
|
>
|
||||||
|
{server.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ export const ShowCertificates = () => {
|
|||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<ShieldCheck className="size-8 self-center text-muted-foreground" />
|
<ShieldCheck className="size-8 self-center text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To create a certificate is required to upload your certificate
|
To create a certificate it is required to upload an existing
|
||||||
|
certificate
|
||||||
</span>
|
</span>
|
||||||
<AddCertificate />
|
<AddCertificate />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,10 +17,18 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, Container } from "lucide-react";
|
import { AlertTriangle, Container } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -36,10 +44,9 @@ const AddRegistrySchema = z.object({
|
|||||||
password: z.string().min(1, {
|
password: z.string().min(1, {
|
||||||
message: "Password is required",
|
message: "Password is required",
|
||||||
}),
|
}),
|
||||||
registryUrl: z.string().min(1, {
|
registryUrl: z.string(),
|
||||||
message: "Registry URL is required",
|
|
||||||
}),
|
|
||||||
imagePrefix: z.string(),
|
imagePrefix: z.string(),
|
||||||
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddRegistry = z.infer<typeof AddRegistrySchema>;
|
type AddRegistry = z.infer<typeof AddRegistrySchema>;
|
||||||
@@ -48,9 +55,9 @@ export const AddRegistry = () => {
|
|||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { mutateAsync, error, isError } = api.registry.create.useMutation();
|
const { mutateAsync, error, isError } = api.registry.create.useMutation();
|
||||||
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
const { mutateAsync: testRegistry, isLoading } =
|
const { mutateAsync: testRegistry, isLoading } =
|
||||||
api.registry.testRegistry.useMutation();
|
api.registry.testRegistry.useMutation();
|
||||||
const router = useRouter();
|
|
||||||
const form = useForm<AddRegistry>({
|
const form = useForm<AddRegistry>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: "",
|
username: "",
|
||||||
@@ -58,6 +65,7 @@ export const AddRegistry = () => {
|
|||||||
registryUrl: "",
|
registryUrl: "",
|
||||||
imagePrefix: "",
|
imagePrefix: "",
|
||||||
registryName: "",
|
registryName: "",
|
||||||
|
serverId: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(AddRegistrySchema),
|
resolver: zodResolver(AddRegistrySchema),
|
||||||
});
|
});
|
||||||
@@ -67,6 +75,7 @@ export const AddRegistry = () => {
|
|||||||
const registryUrl = form.watch("registryUrl");
|
const registryUrl = form.watch("registryUrl");
|
||||||
const registryName = form.watch("registryName");
|
const registryName = form.watch("registryName");
|
||||||
const imagePrefix = form.watch("imagePrefix");
|
const imagePrefix = form.watch("imagePrefix");
|
||||||
|
const serverId = form.watch("serverId");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
@@ -74,6 +83,7 @@ export const AddRegistry = () => {
|
|||||||
password: "",
|
password: "",
|
||||||
registryUrl: "",
|
registryUrl: "",
|
||||||
imagePrefix: "",
|
imagePrefix: "",
|
||||||
|
serverId: "",
|
||||||
});
|
});
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||||
|
|
||||||
@@ -85,6 +95,7 @@ export const AddRegistry = () => {
|
|||||||
registryUrl: data.registryUrl,
|
registryUrl: data.registryUrl,
|
||||||
registryType: "cloud",
|
registryType: "cloud",
|
||||||
imagePrefix: data.imagePrefix,
|
imagePrefix: data.imagePrefix,
|
||||||
|
serverId: data.serverId,
|
||||||
})
|
})
|
||||||
.then(async (data) => {
|
.then(async (data) => {
|
||||||
await utils.registry.all.invalidate();
|
await utils.registry.all.invalidate();
|
||||||
@@ -211,34 +222,77 @@ export const AddRegistry = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className="flex flex-row w-full sm:justify-between gap-4 flex-wrap">
|
<DialogFooter className="flex flex-col w-full sm:justify-between gap-4 flex-wrap sm:flex-col">
|
||||||
<Button
|
<div className="flex flex-col gap-4 border p-2 rounded-lg">
|
||||||
type="button"
|
<span className="text-sm text-muted-foreground">
|
||||||
variant={"secondary"}
|
Select a server to test the registry. If you don't have a
|
||||||
isLoading={isLoading}
|
server choose the default one.
|
||||||
onClick={async () => {
|
</span>
|
||||||
await testRegistry({
|
<FormField
|
||||||
username: username,
|
control={form.control}
|
||||||
password: password,
|
name="serverId"
|
||||||
registryUrl: registryUrl,
|
render={({ field }) => (
|
||||||
registryName: registryName,
|
<FormItem>
|
||||||
registryType: "cloud",
|
<FormLabel>Server (Optional)</FormLabel>
|
||||||
imagePrefix: imagePrefix,
|
<FormControl>
|
||||||
})
|
<Select
|
||||||
.then((data) => {
|
onValueChange={field.onChange}
|
||||||
if (data) {
|
defaultValue={field.value}
|
||||||
toast.success("Registry Tested Successfully");
|
>
|
||||||
} else {
|
<SelectTrigger className="w-full">
|
||||||
toast.error("Registry Test Failed");
|
<SelectValue placeholder="Select a server" />
|
||||||
}
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Servers</SelectLabel>
|
||||||
|
{servers?.map((server) => (
|
||||||
|
<SelectItem
|
||||||
|
key={server.serverId}
|
||||||
|
value={server.serverId}
|
||||||
|
>
|
||||||
|
{server.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectItem value={"none"}>None</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={"secondary"}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onClick={async () => {
|
||||||
|
await testRegistry({
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
registryUrl: registryUrl,
|
||||||
|
registryName: registryName,
|
||||||
|
registryType: "cloud",
|
||||||
|
imagePrefix: imagePrefix,
|
||||||
|
serverId: serverId,
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then((data) => {
|
||||||
toast.error("Error to test the registry");
|
if (data) {
|
||||||
});
|
toast.success("Registry Tested Successfully");
|
||||||
}}
|
} else {
|
||||||
>
|
toast.error("Registry Test Failed");
|
||||||
Test Registry
|
}
|
||||||
</Button>
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to test the registry");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Test Registry
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button isLoading={form.formState.isSubmitting} type="submit">
|
<Button isLoading={form.formState.isSubmitting} type="submit">
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,181 +0,0 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { AlertTriangle, Container } from "lucide-react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const AddRegistrySchema = z.object({
|
|
||||||
username: z
|
|
||||||
.string()
|
|
||||||
.min(1, {
|
|
||||||
message: "Username is required",
|
|
||||||
})
|
|
||||||
.regex(/^[a-zA-Z0-9]+$/, {
|
|
||||||
message: "Username can only contain letters and numbers",
|
|
||||||
}),
|
|
||||||
password: z.string().min(1, {
|
|
||||||
message: "Password is required",
|
|
||||||
}),
|
|
||||||
registryUrl: z.string().min(1, {
|
|
||||||
message: "Registry URL is required",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
type AddRegistry = z.infer<typeof AddRegistrySchema>;
|
|
||||||
|
|
||||||
export const AddSelfHostedRegistry = () => {
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const { mutateAsync, error, isError, isLoading } =
|
|
||||||
api.registry.enableSelfHostedRegistry.useMutation();
|
|
||||||
const router = useRouter();
|
|
||||||
const form = useForm<AddRegistry>({
|
|
||||||
defaultValues: {
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
registryUrl: "",
|
|
||||||
},
|
|
||||||
resolver: zodResolver(AddRegistrySchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
form.reset({
|
|
||||||
registryUrl: "",
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
});
|
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: AddRegistry) => {
|
|
||||||
await mutateAsync({
|
|
||||||
registryUrl: data.registryUrl,
|
|
||||||
username: data.username,
|
|
||||||
password: data.password,
|
|
||||||
})
|
|
||||||
.then(async (data) => {
|
|
||||||
await utils.registry.all.invalidate();
|
|
||||||
toast.success("Self Hosted Registry Created");
|
|
||||||
setIsOpen(false);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to create a self hosted registry");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button className="max-sm:w-full">
|
|
||||||
<Container className="h-4 w-4" />
|
|
||||||
Enable Self Hosted Registry
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:m:max-w-lg ">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Add a self hosted registry</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Fill the next fields to add a self hosted registry.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{isError && (
|
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="grid w-full gap-4"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="username"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Username</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="Username" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="password"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Password</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Password"
|
|
||||||
{...field}
|
|
||||||
type="password"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="registryUrl"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Registry URL</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="registry.dokploy.com" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Point a DNS record to the VPS IP address.
|
|
||||||
</FormDescription>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button isLoading={isLoading} type="submit">
|
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Server } from "lucide-react";
|
import { Server } from "lucide-react";
|
||||||
import { AddRegistry } from "./add-docker-registry";
|
import { AddRegistry } from "./add-docker-registry";
|
||||||
import { AddSelfHostedRegistry } from "./add-self-docker-registry";
|
|
||||||
import { DeleteRegistry } from "./delete-registry";
|
import { DeleteRegistry } from "./delete-registry";
|
||||||
import { UpdateDockerRegistry } from "./update-docker-registry";
|
import { UpdateDockerRegistry } from "./update-docker-registry";
|
||||||
|
|
||||||
@@ -31,8 +30,6 @@ export const ShowRegistry = () => {
|
|||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
{data && data?.length > 0 && (
|
{data && data?.length > 0 && (
|
||||||
<>
|
<>
|
||||||
{!haveSelfHostedRegistry && <AddSelfHostedRegistry />}
|
|
||||||
|
|
||||||
<AddRegistry />
|
<AddRegistry />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -43,11 +40,10 @@ export const ShowRegistry = () => {
|
|||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<Server className="size-8 self-center text-muted-foreground" />
|
<Server className="size-8 self-center text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground text-center">
|
<span className="text-base text-muted-foreground text-center">
|
||||||
To create a cluster is required to set a registry.
|
To create a cluster it is required to set a registry.
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="flex flex-row md:flex-row gap-2 flex-wrap w-full justify-center">
|
<div className="flex flex-row md:flex-row gap-2 flex-wrap w-full justify-center">
|
||||||
<AddSelfHostedRegistry />
|
|
||||||
<AddRegistry />
|
<AddRegistry />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,15 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
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";
|
||||||
@@ -34,10 +43,9 @@ const updateRegistry = z.object({
|
|||||||
message: "Username is required",
|
message: "Username is required",
|
||||||
}),
|
}),
|
||||||
password: z.string(),
|
password: z.string(),
|
||||||
registryUrl: z.string().min(1, {
|
registryUrl: z.string(),
|
||||||
message: "Registry URL is required",
|
|
||||||
}),
|
|
||||||
imagePrefix: z.string(),
|
imagePrefix: z.string(),
|
||||||
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type UpdateRegistry = z.infer<typeof updateRegistry>;
|
type UpdateRegistry = z.infer<typeof updateRegistry>;
|
||||||
@@ -48,6 +56,8 @@ interface Props {
|
|||||||
|
|
||||||
export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
|
|
||||||
const { mutateAsync: testRegistry, isLoading } =
|
const { mutateAsync: testRegistry, isLoading } =
|
||||||
api.registry.testRegistry.useMutation();
|
api.registry.testRegistry.useMutation();
|
||||||
const { data, refetch } = api.registry.one.useQuery(
|
const { data, refetch } = api.registry.one.useQuery(
|
||||||
@@ -69,15 +79,19 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
|||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
registryUrl: "",
|
registryUrl: "",
|
||||||
|
serverId: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(updateRegistry),
|
resolver: zodResolver(updateRegistry),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(form.formState.errors);
|
||||||
|
|
||||||
const password = form.watch("password");
|
const password = form.watch("password");
|
||||||
const username = form.watch("username");
|
const username = form.watch("username");
|
||||||
const registryUrl = form.watch("registryUrl");
|
const registryUrl = form.watch("registryUrl");
|
||||||
const registryName = form.watch("registryName");
|
const registryName = form.watch("registryName");
|
||||||
const imagePrefix = form.watch("imagePrefix");
|
const imagePrefix = form.watch("imagePrefix");
|
||||||
|
const serverId = form.watch("serverId");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -87,6 +101,7 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
|||||||
username: data.username || "",
|
username: data.username || "",
|
||||||
password: "",
|
password: "",
|
||||||
registryUrl: data.registryUrl || "",
|
registryUrl: data.registryUrl || "",
|
||||||
|
serverId: "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.reset, data]);
|
}, [form, form.reset, data]);
|
||||||
@@ -99,6 +114,7 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
|||||||
username: data.username,
|
username: data.username,
|
||||||
registryUrl: data.registryUrl,
|
registryUrl: data.registryUrl,
|
||||||
imagePrefix: data.imagePrefix,
|
imagePrefix: data.imagePrefix,
|
||||||
|
serverId: data.serverId,
|
||||||
})
|
})
|
||||||
.then(async (data) => {
|
.then(async (data) => {
|
||||||
toast.success("Registry Updated");
|
toast.success("Registry Updated");
|
||||||
@@ -224,13 +240,47 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter
|
<DialogFooter className="flex flex-col w-full sm:justify-between gap-4 flex-wrap sm:flex-col">
|
||||||
className={cn(
|
<div className="flex flex-col gap-4 border p-2 rounded-lg">
|
||||||
isCloud ? "sm:justify-between " : "",
|
<span className="text-sm text-muted-foreground">
|
||||||
"flex flex-row w-full gap-4 flex-wrap",
|
Select a server to test the registry. If you don't have a server
|
||||||
)}
|
choose the default one.
|
||||||
>
|
</span>
|
||||||
{isCloud && (
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="serverId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Server (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select a server" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Servers</SelectLabel>
|
||||||
|
{servers?.map((server) => (
|
||||||
|
<SelectItem
|
||||||
|
key={server.serverId}
|
||||||
|
value={server.serverId}
|
||||||
|
>
|
||||||
|
{server.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectItem value={"none"}>None</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={"secondary"}
|
variant={"secondary"}
|
||||||
@@ -243,6 +293,7 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
|||||||
registryName: registryName,
|
registryName: registryName,
|
||||||
registryType: "cloud",
|
registryType: "cloud",
|
||||||
imagePrefix: imagePrefix,
|
imagePrefix: imagePrefix,
|
||||||
|
serverId: serverId,
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -258,12 +309,12 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
|||||||
>
|
>
|
||||||
Test Registry
|
Test Registry
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
isLoading={form.formState.isSubmitting}
|
isLoading={form.formState.isSubmitting}
|
||||||
form="hook-form"
|
|
||||||
type="submit"
|
type="submit"
|
||||||
|
form="hook-form"
|
||||||
>
|
>
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
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";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
@@ -32,12 +42,15 @@ const addDestination = z.object({
|
|||||||
bucket: z.string(),
|
bucket: z.string(),
|
||||||
region: z.string(),
|
region: z.string(),
|
||||||
endpoint: z.string(),
|
endpoint: z.string(),
|
||||||
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddDestination = z.infer<typeof addDestination>;
|
type AddDestination = z.infer<typeof addDestination>;
|
||||||
|
|
||||||
export const AddDestination = () => {
|
export const AddDestination = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
const { mutateAsync, isError, error, isLoading } =
|
const { mutateAsync, isError, error, isLoading } =
|
||||||
api.destination.create.useMutation();
|
api.destination.create.useMutation();
|
||||||
@@ -189,30 +202,106 @@ export const AddDestination = () => {
|
|||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
<DialogFooter
|
||||||
<Button
|
className={cn(
|
||||||
isLoading={isLoadingConnection}
|
isCloud ? "!flex-col" : "flex-row",
|
||||||
type="button"
|
"flex w-full !justify-between pt-3 gap-4",
|
||||||
variant="secondary"
|
)}
|
||||||
onClick={async () => {
|
>
|
||||||
await testConnection({
|
{isCloud ? (
|
||||||
accessKey: form.getValues("accessKeyId"),
|
<div className="flex flex-col gap-4 border p-2 rounded-lg">
|
||||||
bucket: form.getValues("bucket"),
|
<span className="text-sm text-muted-foreground">
|
||||||
endpoint: form.getValues("endpoint"),
|
Select a server to test the destination. If you don't have a
|
||||||
name: "Test",
|
server choose the default one.
|
||||||
region: form.getValues("region"),
|
</span>
|
||||||
secretAccessKey: form.getValues("secretAccessKey"),
|
<FormField
|
||||||
})
|
control={form.control}
|
||||||
.then(async () => {
|
name="serverId"
|
||||||
toast.success("Connection Success");
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Server (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select a server" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Servers</SelectLabel>
|
||||||
|
{servers?.map((server) => (
|
||||||
|
<SelectItem
|
||||||
|
key={server.serverId}
|
||||||
|
value={server.serverId}
|
||||||
|
>
|
||||||
|
{server.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectItem value={"none"}>None</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={"secondary"}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onClick={async () => {
|
||||||
|
await testConnection({
|
||||||
|
accessKey: form.getValues("accessKeyId"),
|
||||||
|
bucket: form.getValues("bucket"),
|
||||||
|
endpoint: form.getValues("endpoint"),
|
||||||
|
name: "Test",
|
||||||
|
region: form.getValues("region"),
|
||||||
|
secretAccessKey: form.getValues("secretAccessKey"),
|
||||||
|
serverId: form.getValues("serverId"),
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Connection Success");
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
toast.error("Error to connect the provider", {
|
||||||
|
description: e.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Test Connection
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
isLoading={isLoadingConnection}
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={async () => {
|
||||||
|
await testConnection({
|
||||||
|
accessKey: form.getValues("accessKeyId"),
|
||||||
|
bucket: form.getValues("bucket"),
|
||||||
|
endpoint: form.getValues("endpoint"),
|
||||||
|
name: "Test",
|
||||||
|
region: form.getValues("region"),
|
||||||
|
secretAccessKey: form.getValues("secretAccessKey"),
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(async () => {
|
||||||
toast.error("Error to connect the provider");
|
toast.success("Connection Success");
|
||||||
});
|
})
|
||||||
}}
|
.catch(() => {
|
||||||
>
|
toast.error("Error to connect the provider");
|
||||||
Test connection
|
});
|
||||||
</Button>
|
}}
|
||||||
|
>
|
||||||
|
Test connection
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
form="hook-form-destination-add"
|
form="hook-form-destination-add"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const ShowDestinations = () => {
|
|||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<FolderUp className="size-8 self-center text-muted-foreground" />
|
<FolderUp className="size-8 self-center text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To create a backup is required to set at least 1 provider.
|
To create a backup it is required to set at least 1 provider.
|
||||||
</span>
|
</span>
|
||||||
<AddDestination />
|
<AddDestination />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
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";
|
||||||
import { PenBoxIcon } from "lucide-react";
|
import { PenBoxIcon } from "lucide-react";
|
||||||
@@ -33,6 +43,7 @@ const updateDestination = z.object({
|
|||||||
bucket: z.string(),
|
bucket: z.string(),
|
||||||
region: z.string(),
|
region: z.string(),
|
||||||
endpoint: z.string(),
|
endpoint: z.string(),
|
||||||
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type UpdateDestination = z.infer<typeof updateDestination>;
|
type UpdateDestination = z.infer<typeof updateDestination>;
|
||||||
@@ -43,6 +54,8 @@ interface Props {
|
|||||||
|
|
||||||
export const UpdateDestination = ({ destinationId }: Props) => {
|
export const UpdateDestination = ({ destinationId }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { data, refetch } = api.destination.one.useQuery(
|
const { data, refetch } = api.destination.one.useQuery(
|
||||||
{
|
{
|
||||||
@@ -220,34 +233,107 @@ export const UpdateDestination = ({ destinationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
<DialogFooter
|
||||||
<Button
|
className={cn(
|
||||||
isLoading={isLoadingConnection}
|
isCloud ? "!flex-col" : "flex-row",
|
||||||
type="button"
|
"flex w-full !justify-between pt-3 gap-4",
|
||||||
variant="secondary"
|
)}
|
||||||
onClick={async () => {
|
>
|
||||||
await testConnection({
|
{isCloud ? (
|
||||||
accessKey: form.getValues("accessKeyId"),
|
<div className="flex flex-col gap-4 border p-2 rounded-lg">
|
||||||
bucket: form.getValues("bucket"),
|
<span className="text-sm text-muted-foreground">
|
||||||
endpoint: form.getValues("endpoint"),
|
Select a server to test the destination. If you don't have a
|
||||||
name: "Test",
|
server choose the default one.
|
||||||
region: form.getValues("region"),
|
</span>
|
||||||
secretAccessKey: form.getValues("secretAccessKey"),
|
<FormField
|
||||||
})
|
control={form.control}
|
||||||
.then(async () => {
|
name="serverId"
|
||||||
toast.success("Connection Success");
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Server (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select a server" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Servers</SelectLabel>
|
||||||
|
{servers?.map((server) => (
|
||||||
|
<SelectItem
|
||||||
|
key={server.serverId}
|
||||||
|
value={server.serverId}
|
||||||
|
>
|
||||||
|
{server.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectItem value={"none"}>None</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={"secondary"}
|
||||||
|
onClick={async () => {
|
||||||
|
await testConnection({
|
||||||
|
accessKey: form.getValues("accessKeyId"),
|
||||||
|
bucket: form.getValues("bucket"),
|
||||||
|
endpoint: form.getValues("endpoint"),
|
||||||
|
name: "Test",
|
||||||
|
region: form.getValues("region"),
|
||||||
|
secretAccessKey: form.getValues("secretAccessKey"),
|
||||||
|
serverId: form.getValues("serverId"),
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Connection Success");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to connect the provider");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Test Connection
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
isLoading={isLoadingConnection}
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={async () => {
|
||||||
|
await testConnection({
|
||||||
|
accessKey: form.getValues("accessKeyId"),
|
||||||
|
bucket: form.getValues("bucket"),
|
||||||
|
endpoint: form.getValues("endpoint"),
|
||||||
|
name: "Test",
|
||||||
|
region: form.getValues("region"),
|
||||||
|
secretAccessKey: form.getValues("secretAccessKey"),
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(async () => {
|
||||||
toast.error("Error to connect the provider");
|
toast.success("Connection Success");
|
||||||
});
|
})
|
||||||
}}
|
.catch(() => {
|
||||||
>
|
toast.error("Error to connect the provider");
|
||||||
Test connection
|
});
|
||||||
</Button>
|
}}
|
||||||
|
>
|
||||||
|
Test connection
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
isLoading={form.formState.isSubmitting}
|
|
||||||
form="hook-form"
|
form="hook-form"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
isLoading={form.formState.isSubmitting}
|
||||||
>
|
>
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -11,13 +11,11 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { useUrl } from "@/utils/hooks/use-url";
|
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export const AddGithubProvider = () => {
|
export const AddGithubProvider = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const url = useUrl();
|
|
||||||
const { data } = api.auth.get.useQuery();
|
const { data } = api.auth.get.useQuery();
|
||||||
const [manifest, setManifest] = useState("");
|
const [manifest, setManifest] = useState("");
|
||||||
const [isOrganization, setIsOrganization] = useState(false);
|
const [isOrganization, setIsOrganization] = useState(false);
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export type NotificationSchema = z.infer<typeof notificationSchema>;
|
|||||||
export const AddNotification = () => {
|
export const AddNotification = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } =
|
const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } =
|
||||||
api.notification.testSlackConnection.useMutation();
|
api.notification.testSlackConnection.useMutation();
|
||||||
|
|
||||||
@@ -660,26 +660,28 @@ export const AddNotification = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
{!isCloud && (
|
||||||
control={form.control}
|
<FormField
|
||||||
name="dokployRestart"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="dokployRestart"
|
||||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
render={({ field }) => (
|
||||||
<div className="space-y-0.5">
|
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
<FormLabel>Dokploy Restart</FormLabel>
|
<div className="space-y-0.5">
|
||||||
<FormDescription>
|
<FormLabel>Dokploy Restart</FormLabel>
|
||||||
Trigger the action when a dokploy is restarted.
|
<FormDescription>
|
||||||
</FormDescription>
|
Trigger the action when a dokploy is restarted.
|
||||||
</div>
|
</FormDescription>
|
||||||
<FormControl>
|
</div>
|
||||||
<Switch
|
<FormControl>
|
||||||
checked={field.value}
|
<Switch
|
||||||
onCheckedChange={field.onChange}
|
checked={field.value}
|
||||||
/>
|
onCheckedChange={field.onChange}
|
||||||
</FormControl>
|
/>
|
||||||
</FormItem>
|
</FormControl>
|
||||||
)}
|
</FormItem>
|
||||||
/>
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export const ShowNotifications = () => {
|
|||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<BellRing className="size-8 self-center text-muted-foreground" />
|
<BellRing className="size-8 self-center text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To send notifications is required to set at least 1 provider.
|
To send notifications it is required to set at least 1 provider.
|
||||||
</span>
|
</span>
|
||||||
<AddNotification />
|
<AddNotification />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user