mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
299 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
572579af91 | ||
|
|
63998f71ec | ||
|
|
45fd2d149c | ||
|
|
9a35c85277 | ||
|
|
0cf21cf3f7 | ||
|
|
7400913646 | ||
|
|
e78d354d0d | ||
|
|
bec3ad6bb5 | ||
|
|
5846e429e5 | ||
|
|
0f7652d02c | ||
|
|
fef19056fa | ||
|
|
b07d9939a6 | ||
|
|
301e3480e4 | ||
|
|
976ac053f7 | ||
|
|
f102bae5d5 | ||
|
|
00883dde11 | ||
|
|
e194f3c454 | ||
|
|
cdd39670f5 | ||
|
|
88f7cf2546 | ||
|
|
34ea7ad8c9 | ||
|
|
081a2d8f69 | ||
|
|
a6368ee0b8 | ||
|
|
4132f714ae | ||
|
|
333776a5a1 | ||
|
|
5853117e5f | ||
|
|
9e0e3540f5 | ||
|
|
7bd6e7fd9a | ||
|
|
95ab6af3ac | ||
|
|
69876029b1 | ||
|
|
d4fdf881cd | ||
|
|
3b14ebcaa4 | ||
|
|
22b8fa2c00 | ||
|
|
714865730f | ||
|
|
7469c30992 | ||
|
|
b296b6bbf0 | ||
|
|
37fa139a65 | ||
|
|
a1cf597c2b | ||
|
|
c8e9d9d169 | ||
|
|
d01928a878 | ||
|
|
30c19c5698 | ||
|
|
01dfa7feaf | ||
|
|
58e6462ff1 | ||
|
|
d18876d4fb | ||
|
|
492c912c61 | ||
|
|
6a283c8ee2 | ||
|
|
59dfdd6192 | ||
|
|
3c072d7aa8 | ||
|
|
19d897f3ad | ||
|
|
0477329db7 | ||
|
|
fabe946526 | ||
|
|
daa0c9d5d4 | ||
|
|
afe9b3c113 | ||
|
|
cbfd09786a | ||
|
|
54eb5544ac | ||
|
|
ac33b6b6a1 | ||
|
|
653b1972ca | ||
|
|
7d7eb6a7a2 | ||
|
|
fab7e138b7 | ||
|
|
62b635b2f0 | ||
|
|
2dd352ee76 | ||
|
|
422b6eea82 | ||
|
|
4850305fb6 | ||
|
|
97779f5686 | ||
|
|
d4b8985d71 | ||
|
|
d5686063e0 | ||
|
|
62ca8eec53 | ||
|
|
204143648d | ||
|
|
be8bd78bcc | ||
|
|
9003e43702 | ||
|
|
55ac24ee8e | ||
|
|
f3be56234b | ||
|
|
fd59beaff1 | ||
|
|
4a70d60aed | ||
|
|
f7533c88f6 | ||
|
|
ea8cae7815 | ||
|
|
e9956a66da | ||
|
|
2eeb4017ac | ||
|
|
28f0c9f162 | ||
|
|
4967d3bb31 | ||
|
|
238fa5d02d | ||
|
|
d1436c992e | ||
|
|
0654804821 | ||
|
|
c0876044b0 | ||
|
|
6dff11af22 | ||
|
|
6d674a4c6b | ||
|
|
96b2579d69 | ||
|
|
0708fa05b6 | ||
|
|
597842a99f | ||
|
|
105cf1014f | ||
|
|
b93f36ae77 | ||
|
|
bebb4b973c | ||
|
|
f790530d4d | ||
|
|
b7374549b8 | ||
|
|
366e881d72 | ||
|
|
c2125d82b1 | ||
|
|
32b3a76457 | ||
|
|
5cbdc8fad9 | ||
|
|
3698e8a827 | ||
|
|
a83b62f62b | ||
|
|
ac033cea22 | ||
|
|
58814239d9 | ||
|
|
6fc1ce2fbc | ||
|
|
adde8126ab | ||
|
|
cda66606ec | ||
|
|
28f2c1a3c0 | ||
|
|
1c5fe8a283 | ||
|
|
da005bc511 | ||
|
|
5c8721406a | ||
|
|
5db5336ec8 | ||
|
|
a6e7edd4d9 | ||
|
|
ddbb414225 | ||
|
|
b73889dd4f | ||
|
|
ce2dce3401 | ||
|
|
173110a415 | ||
|
|
2307346ae3 | ||
|
|
2f175f0e44 | ||
|
|
7003fe77c9 | ||
|
|
04235fb6c9 | ||
|
|
f138b0917f | ||
|
|
82367213ea | ||
|
|
3c490ba2d0 | ||
|
|
4bf5e5ca06 | ||
|
|
6af5742702 | ||
|
|
3015d69adc | ||
|
|
f7fa8e74af | ||
|
|
2835c997e9 | ||
|
|
bd55e3751f | ||
|
|
74374bd643 | ||
|
|
4e929c12f2 | ||
|
|
56ea356723 | ||
|
|
b1f7d05743 | ||
|
|
58338380ab | ||
|
|
2487e3e062 | ||
|
|
01a882497f | ||
|
|
036313e3c3 | ||
|
|
d3304052b0 | ||
|
|
9efd2e3d5c | ||
|
|
1e7d9fc3aa | ||
|
|
70ca219c0a | ||
|
|
3a07d8de2e | ||
|
|
f4f1fc28a0 | ||
|
|
4c71d3a95f | ||
|
|
af84942d22 | ||
|
|
1aaff0594d | ||
|
|
69a4a87079 | ||
|
|
3eef4aa016 | ||
|
|
2492581bde | ||
|
|
be934065d9 | ||
|
|
82fc9897d2 | ||
|
|
8162dcfb71 | ||
|
|
b6ab653ef3 | ||
|
|
17e9a1a497 | ||
|
|
46f7d43595 | ||
|
|
6961ee1fc0 | ||
|
|
8e532d5a60 | ||
|
|
fa791706a0 | ||
|
|
a5c1f8ef49 | ||
|
|
6866c3b116 | ||
|
|
444302e7b9 | ||
|
|
9b77573269 | ||
|
|
8157dd9eaa | ||
|
|
f1fc3f161a | ||
|
|
06af2042ee | ||
|
|
8900e30ae7 | ||
|
|
b5fa411093 | ||
|
|
cab9443d25 | ||
|
|
de315124c3 | ||
|
|
7bef3a0c29 | ||
|
|
82afd486da | ||
|
|
69c9e86a13 | ||
|
|
3d59e289be | ||
|
|
59bb59ee24 | ||
|
|
8d33ff5fb5 | ||
|
|
46219e1b3d | ||
|
|
3457de4f36 | ||
|
|
e57efa2e31 | ||
|
|
fb0308fd60 | ||
|
|
b376ead7b5 | ||
|
|
d081d477ef | ||
|
|
cf73f1f764 | ||
|
|
deeea11428 | ||
|
|
5c17797749 | ||
|
|
7b06fd47b8 | ||
|
|
faceed12b0 | ||
|
|
814580ff2c | ||
|
|
4f092b2fb3 | ||
|
|
0799f8e04c | ||
|
|
59c050b519 | ||
|
|
88f969917f | ||
|
|
ed470ee827 | ||
|
|
96584e5b32 | ||
|
|
a7165bef20 | ||
|
|
b0f5e7dad3 | ||
|
|
fa083257f1 | ||
|
|
5cf12e51d1 | ||
|
|
b6fd410af2 | ||
|
|
c5c3ca39cd | ||
|
|
0e433a3d36 | ||
|
|
b08a2f54f0 | ||
|
|
29ffdf2c71 | ||
|
|
58b185f6dd | ||
|
|
5f6d041248 | ||
|
|
b817b4b6ee | ||
|
|
819de5a32e | ||
|
|
e0fe4e4995 | ||
|
|
461d30fc26 | ||
|
|
3fee18d06e | ||
|
|
046923aeb3 | ||
|
|
4646b7d0ec | ||
|
|
a02d51e504 | ||
|
|
9728c49edd | ||
|
|
ed5b01c78d | ||
|
|
000091cfb9 | ||
|
|
2774701895 | ||
|
|
e238dd8510 | ||
|
|
c09ff25360 | ||
|
|
56b565b512 | ||
|
|
046f0a5c20 | ||
|
|
66c4d8f118 | ||
|
|
7f0a92f224 | ||
|
|
0ca8ee17be | ||
|
|
c765d7d9eb | ||
|
|
237106428b | ||
|
|
06e1e1ba76 | ||
|
|
9eb4c3e77d | ||
|
|
f3d8351208 | ||
|
|
a331020bf8 | ||
|
|
2e6d9c34c0 | ||
|
|
1e1409e651 | ||
|
|
ceaa32fd00 | ||
|
|
f466e697dd | ||
|
|
476057663b | ||
|
|
b53da82204 | ||
|
|
3b5e8921d0 | ||
|
|
1b1d0597fe | ||
|
|
dfa73a3d7c | ||
|
|
2a24e1d7e8 | ||
|
|
5cd624c7ea | ||
|
|
34b12a0315 | ||
|
|
e9e6064eb6 | ||
|
|
6b7712e35f | ||
|
|
7306d8c513 | ||
|
|
84aac40410 | ||
|
|
e40a0fdc50 | ||
|
|
e3677995b9 | ||
|
|
0468c25bf8 | ||
|
|
bc097c7667 | ||
|
|
89c7e96df0 | ||
|
|
8af5afbb6c | ||
|
|
ed7150fac1 | ||
|
|
ef9f16a3c3 | ||
|
|
d15b6387a3 | ||
|
|
2e9e39dcf5 | ||
|
|
d901b02e92 | ||
|
|
766a25ccad | ||
|
|
1b6d8d803b | ||
|
|
fae97b1817 | ||
|
|
b2cc5e58a3 | ||
|
|
2f8b89c8a6 | ||
|
|
2da650610c | ||
|
|
980024c9d2 | ||
|
|
1b56a6b400 | ||
|
|
996d449f0f | ||
|
|
3b197f3624 | ||
|
|
bba7d0c828 | ||
|
|
a04b69d0fd | ||
|
|
0e136ffb8f | ||
|
|
95e53169b1 | ||
|
|
de02a00ce9 | ||
|
|
25803f371c | ||
|
|
a4eb5c07e6 | ||
|
|
bad11f13f5 | ||
|
|
02d52d63b9 | ||
|
|
2821e43cdd | ||
|
|
adea440931 | ||
|
|
bbef99c3c2 | ||
|
|
527c01e7dc | ||
|
|
2a5a67e63c | ||
|
|
15051a1bc2 | ||
|
|
b7d45341bc | ||
|
|
1695c7cc81 | ||
|
|
3e467959c9 | ||
|
|
fbec26fc31 | ||
|
|
b3092691b7 | ||
|
|
5a440d934d | ||
|
|
96c5176984 | ||
|
|
433430118f | ||
|
|
4431f5601a | ||
|
|
bf48aa03a2 | ||
|
|
6d3ea8df59 | ||
|
|
e52a0fc9d4 | ||
|
|
15a76d2639 | ||
|
|
059c21c990 | ||
|
|
d833623ebf | ||
|
|
8b855d7ee4 | ||
|
|
706cde4ffd | ||
|
|
a39a7a276d | ||
|
|
0327334fcd | ||
|
|
3e0d4ebbd6 |
@@ -18,8 +18,10 @@ jobs:
|
|||||||
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
||||||
if [ "${CIRCLE_BRANCH}" == "main" ]; then
|
if [ "${CIRCLE_BRANCH}" == "main" ]; then
|
||||||
TAG="latest"
|
TAG="latest"
|
||||||
else
|
elif [ "${CIRCLE_BRANCH}" == "canary" ]; then
|
||||||
TAG="canary"
|
TAG="canary"
|
||||||
|
else
|
||||||
|
TAG="feature"
|
||||||
fi
|
fi
|
||||||
docker build --platform linux/amd64 -t dokploy/dokploy:${TAG}-amd64 .
|
docker build --platform linux/amd64 -t dokploy/dokploy:${TAG}-amd64 .
|
||||||
docker push dokploy/dokploy:${TAG}-amd64
|
docker push dokploy/dokploy:${TAG}-amd64
|
||||||
@@ -41,8 +43,10 @@ jobs:
|
|||||||
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
||||||
if [ "${CIRCLE_BRANCH}" == "main" ]; then
|
if [ "${CIRCLE_BRANCH}" == "main" ]; then
|
||||||
TAG="latest"
|
TAG="latest"
|
||||||
else
|
elif [ "${CIRCLE_BRANCH}" == "canary" ]; then
|
||||||
TAG="canary"
|
TAG="canary"
|
||||||
|
else
|
||||||
|
TAG="feature"
|
||||||
fi
|
fi
|
||||||
docker build --platform linux/arm64 -t dokploy/dokploy:${TAG}-arm64 .
|
docker build --platform linux/arm64 -t dokploy/dokploy:${TAG}-arm64 .
|
||||||
docker push dokploy/dokploy:${TAG}-arm64
|
docker push dokploy/dokploy:${TAG}-arm64
|
||||||
@@ -72,12 +76,18 @@ jobs:
|
|||||||
dokploy/dokploy:${TAG}-amd64 \
|
dokploy/dokploy:${TAG}-amd64 \
|
||||||
dokploy/dokploy:${TAG}-arm64
|
dokploy/dokploy:${TAG}-arm64
|
||||||
docker manifest push dokploy/dokploy:${VERSION}
|
docker manifest push dokploy/dokploy:${VERSION}
|
||||||
else
|
elif [ "${CIRCLE_BRANCH}" == "canary" ]; then
|
||||||
TAG="canary"
|
TAG="canary"
|
||||||
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
|
||||||
docker manifest push dokploy/dokploy:${TAG}
|
docker manifest push dokploy/dokploy:${TAG}
|
||||||
|
else
|
||||||
|
TAG="feature"
|
||||||
|
docker manifest create dokploy/dokploy:${TAG} \
|
||||||
|
dokploy/dokploy:${TAG}-amd64 \
|
||||||
|
dokploy/dokploy:${TAG}-arm64
|
||||||
|
docker manifest push dokploy/dokploy:${TAG}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
@@ -89,12 +99,14 @@ workflows:
|
|||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
- canary
|
- canary
|
||||||
|
- fix/build-i18n
|
||||||
- build-arm64:
|
- build-arm64:
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
- canary
|
- canary
|
||||||
|
- fix/build-i18n
|
||||||
- combine-manifests:
|
- combine-manifests:
|
||||||
requires:
|
requires:
|
||||||
- build-amd64
|
- build-amd64
|
||||||
@@ -104,3 +116,4 @@ workflows:
|
|||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
- canary
|
- canary
|
||||||
|
- fix/build-i18n
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
pnpm commitlint --edit $1
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// 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());
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pnpm lint-staged
|
|
||||||
52
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
52
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: Create a bug report
|
description: Create a bug report
|
||||||
labels: ['bug']
|
labels: ["bug"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
@@ -11,18 +11,27 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: To Reproduce
|
label: To Reproduce
|
||||||
description: A step-by-step description of how to reproduce the issue, or a link to the reproducible repository.
|
description: |
|
||||||
|
A detailed, step-by-step description of how to reproduce the issue is required.
|
||||||
|
Please ensure your report includes clear instructions using numbered lists.
|
||||||
|
|
||||||
|
If possible, provide a link to a repository or project where the issue can be reproduced.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
1. Create a application
|
1. Create a application
|
||||||
2. Click X
|
2. Click X
|
||||||
3. Y will happen
|
3. Y will happen
|
||||||
|
|
||||||
|
Make sure to:
|
||||||
|
- Use numbered lists to outline steps clearly.
|
||||||
|
- Include all relevant commands and configurations.
|
||||||
|
- Provide a link to a reproducible repository if applicable.
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Current vs. Expected behavior
|
label: Current vs. Expected behavior
|
||||||
description: A clear and concise description of what the bug is, and what you expected to happen.
|
description: A clear and concise description of what the bug is, and what you expected to happen.
|
||||||
placeholder: 'Following the steps from the previous section, I expected A to happen, but I observed B instead'
|
placeholder: "Following the steps from the previous section, I expected A to happen, but I observed B instead"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@@ -45,12 +54,23 @@ body:
|
|||||||
label: Which area(s) are affected? (Select all that apply)
|
label: Which area(s) are affected? (Select all that apply)
|
||||||
multiple: true
|
multiple: true
|
||||||
options:
|
options:
|
||||||
- 'Installation'
|
- "Installation"
|
||||||
- 'Application'
|
- "Application"
|
||||||
- 'Databases'
|
- "Databases"
|
||||||
- 'Docker Compose'
|
- "Docker Compose"
|
||||||
- 'Traefik'
|
- "Traefik"
|
||||||
- 'Docker'
|
- "Docker"
|
||||||
|
- "Remote server"
|
||||||
|
- "Local Development"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Are you deploying the applications where Dokploy is installed or on a remote server?
|
||||||
|
options:
|
||||||
|
- "Same server where Dokploy is installed"
|
||||||
|
- "Remote server"
|
||||||
|
- "Both"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@@ -59,4 +79,16 @@ body:
|
|||||||
description: |
|
description: |
|
||||||
Any extra information that might help us investigate.
|
Any extra information that might help us investigate.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
I tested on a DigitalOcean VPS with Ubuntu 20.04 and Docker version 20.10.12.
|
I tested on a DigitalOcean VPS with Ubuntu 20.04 and Docker version 20.10.12.
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Will you send a PR to fix it?
|
||||||
|
description: Let us know if you are planning to submit a pull request to address this issue.
|
||||||
|
|
||||||
|
options:
|
||||||
|
- "Yes"
|
||||||
|
- "No"
|
||||||
|
- "Maybe, need help"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|||||||
15
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
15
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: Feature Request
|
name: Feature Request
|
||||||
description: Suggest a new feature or improvement to the project
|
description: Suggest a new feature or improvement to the project
|
||||||
labels: ['enhancement']
|
labels: ["enhancement"]
|
||||||
body:
|
body:
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
@@ -30,4 +30,15 @@ body:
|
|||||||
label: Additional context
|
label: Additional context
|
||||||
description: Add any other context or screenshots about the feature request here.
|
description: Add any other context or screenshots about the feature request here.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Will you send a PR to implement it?
|
||||||
|
description: Let us know if you are planning to submit a pull request to implement this feature.
|
||||||
|
options:
|
||||||
|
- "Yes"
|
||||||
|
- "No"
|
||||||
|
- "Maybe, need help"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|||||||
BIN
.github/sponsors/mandarin.png
vendored
Normal file
BIN
.github/sponsors/mandarin.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
.github/sponsors/startupfame.png
vendored
Normal file
BIN
.github/sponsors/startupfame.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
8
.github/workflows/pull-request.yml
vendored
8
.github/workflows/pull-request.yml
vendored
@@ -4,9 +4,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, canary]
|
branches: [main, canary]
|
||||||
|
|
||||||
env:
|
|
||||||
HUSKY: 0
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint-and-typecheck:
|
lint-and-typecheck:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -18,8 +15,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 run server:build
|
||||||
- run: pnpm biome ci
|
|
||||||
- run: pnpm typecheck
|
- run: pnpm typecheck
|
||||||
|
|
||||||
build-and-test:
|
build-and-test:
|
||||||
@@ -46,5 +42,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 run server:build
|
||||||
- run: pnpm test
|
- run: pnpm test
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
npx commitlint --edit "$1"
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// 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());
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
pnpm run check
|
|
||||||
git add .
|
|
||||||
@@ -14,10 +14,12 @@ We have a few guidelines to follow when contributing to this project:
|
|||||||
|
|
||||||
## Commit Convention
|
## Commit Convention
|
||||||
|
|
||||||
|
|
||||||
Before you create a Pull Request, please make sure your commit message follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
|
Before you create a Pull Request, please make sure your commit message follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
|
||||||
|
|
||||||
### Commit Message Format
|
### Commit Message Format
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
<type>[optional scope]: <description>
|
<type>[optional scope]: <description>
|
||||||
|
|
||||||
@@ -235,7 +237,7 @@ export function generate(schema: Schema): Template {
|
|||||||
|
|
||||||
5. Add the logo or image of the template to `public/templates/plausible.svg`
|
5. Add the logo or image of the template to `public/templates/plausible.svg`
|
||||||
|
|
||||||
### Recomendations
|
### Recommendations
|
||||||
|
|
||||||
- Use the same name of the folder as the id of the template.
|
- Use the same name of the folder as the id of the template.
|
||||||
- The logo should be in the public folder.
|
- The logo should be in the public folder.
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var
|
|||||||
COPY --from=build /prod/dokploy/.next ./.next
|
COPY --from=build /prod/dokploy/.next ./.next
|
||||||
COPY --from=build /prod/dokploy/dist ./dist
|
COPY --from=build /prod/dokploy/dist ./dist
|
||||||
COPY --from=build /prod/dokploy/next.config.mjs ./next.config.mjs
|
COPY --from=build /prod/dokploy/next.config.mjs ./next.config.mjs
|
||||||
|
COPY --from=build /prod/dokploy/next-i18next.config.cjs ./next-i18next.config.cjs
|
||||||
COPY --from=build /prod/dokploy/public ./public
|
COPY --from=build /prod/dokploy/public ./public
|
||||||
COPY --from=build /prod/dokploy/package.json ./package.json
|
COPY --from=build /prod/dokploy/package.json ./package.json
|
||||||
COPY --from=build /prod/dokploy/drizzle ./drizzle
|
COPY --from=build /prod/dokploy/drizzle ./drizzle
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var
|
|||||||
COPY --from=build /prod/dokploy/.next ./.next
|
COPY --from=build /prod/dokploy/.next ./.next
|
||||||
COPY --from=build /prod/dokploy/dist ./dist
|
COPY --from=build /prod/dokploy/dist ./dist
|
||||||
COPY --from=build /prod/dokploy/next.config.mjs ./next.config.mjs
|
COPY --from=build /prod/dokploy/next.config.mjs ./next.config.mjs
|
||||||
|
COPY --from=build /prod/dokploy/next-i18next.config.cjs ./next-i18next.config.cjs
|
||||||
COPY --from=build /prod/dokploy/public ./public
|
COPY --from=build /prod/dokploy/public ./public
|
||||||
COPY --from=build /prod/dokploy/package.json ./package.json
|
COPY --from=build /prod/dokploy/package.json ./package.json
|
||||||
COPY --from=build /prod/dokploy/drizzle ./drizzle
|
COPY --from=build /prod/dokploy/drizzle ./drizzle
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -39,6 +39,8 @@ Dokploy includes multiple features to make your life easier.
|
|||||||
|
|
||||||
To get started, run the following command on a VPS:
|
To get started, run the following command on a VPS:
|
||||||
|
|
||||||
|
Want to skip the installation process? [Try the Dokploy Cloud](https://app.dokploy.com).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sSL https://dokploy.com/install.sh | sh
|
curl -sSL https://dokploy.com/install.sh | sh
|
||||||
```
|
```
|
||||||
@@ -60,12 +62,15 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
### Hero Sponsors 🎖
|
### Hero Sponsors 🎖
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; gap: 20px;">
|
<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;">
|
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 10px;">
|
||||||
<img src=".github/sponsors/hostinger.jpg" alt="Hostinger" height="50"/>
|
<img src=".github/sponsors/hostinger.jpg" alt="Hostinger" height="50"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.lxaer.com/?ref=dokploy" target="_blank" style="display: inline-block;">
|
<a href="https://www.lxaer.com/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 10px;">
|
||||||
<img src=".github/sponsors/lxaer.png" alt="LX Aer" height="50"/>
|
<img src=".github/sponsors/lxaer.png" alt="LX Aer" height="50"/>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://mandarin3d.com/?ref=dokploy" target="_blank" style="display: inline-block;">
|
||||||
|
<img src=".github/sponsors/mandarin.png" alt="Mandarin" height="50"/>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### Premium Supporters 🥇
|
### Premium Supporters 🥇
|
||||||
@@ -82,7 +87,8 @@ 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://lightspeed.run/?ref=dokploy"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
|
<a href="https://lightspeed.run/?ref=dokploy"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
|
||||||
<a href="https://cloudblast.io/?ref=dokploy "><img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" width="250px" alt="Lightspeed.run"/></a>
|
<a href="https://cloudblast.io/?ref=dokploy "><img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" width="250px" alt="Cloudblast.io"/></a>
|
||||||
|
<a href="https://startupfa.me/?ref=dokploy "><img src=".github/sponsors/startupfame.png" width="65px" alt="Startupfame"/></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### Community Backers 🤝
|
### Community Backers 🤝
|
||||||
@@ -111,7 +117,7 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
## Video Tutorial
|
## Video Tutorial
|
||||||
|
|
||||||
<a href="https://youtu.be/mznYKPvhcfw">
|
<a href="https://youtu.be/mznYKPvhcfw">
|
||||||
<img src="https://dokploy.com/banner.webp" alt="Watch the video" width="400" style="border-radius:20px;"/>
|
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400" style="border-radius:20px;"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- ## Supported OS
|
<!-- ## Supported OS
|
||||||
|
|||||||
@@ -39,6 +39,24 @@ describe("createDomainLabels", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should add the path prefix if is different than / empty", async () => {
|
||||||
|
const labels = await createDomainLabels(
|
||||||
|
appName,
|
||||||
|
{
|
||||||
|
...baseDomain,
|
||||||
|
path: "/hello",
|
||||||
|
},
|
||||||
|
"websecure",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(labels).toEqual([
|
||||||
|
"traefik.http.routers.test-app-1-websecure.rule=Host(`example.com`) && PathPrefix(`/hello`)",
|
||||||
|
"traefik.http.routers.test-app-1-websecure.entrypoints=websecure",
|
||||||
|
"traefik.http.services.test-app-1-websecure.loadbalancer.server.port=8080",
|
||||||
|
"traefik.http.routers.test-app-1-websecure.service=test-app-1-websecure",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("should add redirect middleware for https on web entrypoint", async () => {
|
it("should add redirect middleware for https on web entrypoint", async () => {
|
||||||
const httpsBaseDomain = { ...baseDomain, https: true };
|
const httpsBaseDomain = { ...baseDomain, https: true };
|
||||||
const labels = await createDomainLabels(appName, httpsBaseDomain, "web");
|
const labels = await createDomainLabels(appName, httpsBaseDomain, "web");
|
||||||
|
|||||||
@@ -30,8 +30,17 @@ const baseApp: ApplicationNested = {
|
|||||||
appName: "",
|
appName: "",
|
||||||
autoDeploy: true,
|
autoDeploy: true,
|
||||||
serverId: "",
|
serverId: "",
|
||||||
|
registryUrl: "",
|
||||||
branch: null,
|
branch: null,
|
||||||
dockerBuildStage: "",
|
dockerBuildStage: "",
|
||||||
|
project: {
|
||||||
|
env: "",
|
||||||
|
adminId: "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
createdAt: "",
|
||||||
|
projectId: "",
|
||||||
|
},
|
||||||
buildArgs: null,
|
buildArgs: null,
|
||||||
buildPath: "/",
|
buildPath: "/",
|
||||||
gitlabPathNamespace: "",
|
gitlabPathNamespace: "",
|
||||||
|
|||||||
179
apps/dokploy/__test__/env/shared.test.ts
vendored
Normal file
179
apps/dokploy/__test__/env/shared.test.ts
vendored
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import { prepareEnvironmentVariables } from "@dokploy/server/index";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const projectEnv = `
|
||||||
|
ENVIRONMENT=staging
|
||||||
|
DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db
|
||||||
|
PORT=3000
|
||||||
|
`;
|
||||||
|
const serviceEnv = `
|
||||||
|
ENVIRONMENT=\${{project.ENVIRONMENT}}
|
||||||
|
DATABASE_URL=\${{project.DATABASE_URL}}
|
||||||
|
SERVICE_PORT=4000
|
||||||
|
`;
|
||||||
|
|
||||||
|
describe("prepareEnvironmentVariables", () => {
|
||||||
|
it("resolves project variables correctly", () => {
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"ENVIRONMENT=staging",
|
||||||
|
"DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db",
|
||||||
|
"SERVICE_PORT=4000",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles undefined project variables", () => {
|
||||||
|
const incompleteProjectEnv = `
|
||||||
|
NODE_ENV=production
|
||||||
|
`;
|
||||||
|
|
||||||
|
const invalidServiceEnv = `
|
||||||
|
UNDEFINED_VAR=\${{project.UNDEFINED_VAR}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() =>
|
||||||
|
prepareEnvironmentVariables(invalidServiceEnv, incompleteProjectEnv), // Cambiado el orden
|
||||||
|
).toThrow("Invalid project environment variable: project.UNDEFINED_VAR");
|
||||||
|
});
|
||||||
|
it("allows service-specific variables to override project variables", () => {
|
||||||
|
const serviceSpecificEnv = `
|
||||||
|
ENVIRONMENT=production
|
||||||
|
DATABASE_URL=\${{project.DATABASE_URL}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariables(
|
||||||
|
serviceSpecificEnv,
|
||||||
|
projectEnv,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"ENVIRONMENT=production", // Overrides project variable
|
||||||
|
"DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves complex references for dynamic endpoints", () => {
|
||||||
|
const projectEnv = `
|
||||||
|
BASE_URL=https://api.example.com
|
||||||
|
API_VERSION=v1
|
||||||
|
PORT=8000
|
||||||
|
`;
|
||||||
|
const serviceEnv = `
|
||||||
|
API_ENDPOINT=\${{project.BASE_URL}}/\${{project.API_VERSION}}/endpoint
|
||||||
|
SERVICE_PORT=9000
|
||||||
|
`;
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"API_ENDPOINT=https://api.example.com/v1/endpoint",
|
||||||
|
"SERVICE_PORT=9000",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles missing project variables gracefully", () => {
|
||||||
|
const projectEnv = `
|
||||||
|
PORT=8080
|
||||||
|
`;
|
||||||
|
const serviceEnv = `
|
||||||
|
MISSING_VAR=\${{project.MISSING_KEY}}
|
||||||
|
SERVICE_PORT=3000
|
||||||
|
`;
|
||||||
|
|
||||||
|
expect(() => prepareEnvironmentVariables(serviceEnv, projectEnv)).toThrow(
|
||||||
|
"Invalid project environment variable: project.MISSING_KEY",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("overrides project variables with service-specific values", () => {
|
||||||
|
const projectEnv = `
|
||||||
|
ENVIRONMENT=staging
|
||||||
|
DATABASE_URL=postgres://project:project@localhost:5432/project_db
|
||||||
|
`;
|
||||||
|
const serviceEnv = `
|
||||||
|
ENVIRONMENT=\${{project.ENVIRONMENT}}
|
||||||
|
DATABASE_URL=postgres://service:service@localhost:5432/service_db
|
||||||
|
SERVICE_NAME=my-service
|
||||||
|
`;
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"ENVIRONMENT=staging",
|
||||||
|
"DATABASE_URL=postgres://service:service@localhost:5432/service_db",
|
||||||
|
"SERVICE_NAME=my-service",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles project variables with normal and unusual characters", () => {
|
||||||
|
const projectEnv = `
|
||||||
|
ENVIRONMENT=PRODUCTION
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Needs to be in quotes
|
||||||
|
const serviceEnv = `
|
||||||
|
NODE_ENV=\${{project.ENVIRONMENT}}
|
||||||
|
SPECIAL_VAR="$^@$^@#$^@!#$@#$-\${{project.ENVIRONMENT}}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"NODE_ENV=PRODUCTION",
|
||||||
|
"SPECIAL_VAR=$^@$^@#$^@!#$@#$-PRODUCTION",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles complex cases with multiple references, special characters, and spaces", () => {
|
||||||
|
const projectEnv = `
|
||||||
|
ENVIRONMENT=STAGING
|
||||||
|
APP_NAME=MyApp
|
||||||
|
`;
|
||||||
|
|
||||||
|
const serviceEnv = `
|
||||||
|
NODE_ENV=\${{project.ENVIRONMENT}}
|
||||||
|
COMPLEX_VAR="Prefix-$#^!@-\${{project.ENVIRONMENT}}--\${{project.APP_NAME}} Suffix "
|
||||||
|
`;
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"NODE_ENV=STAGING",
|
||||||
|
"COMPLEX_VAR=Prefix-$#^!@-STAGING--MyApp Suffix ",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles references enclosed in single quotes", () => {
|
||||||
|
const projectEnv = `
|
||||||
|
ENVIRONMENT=STAGING
|
||||||
|
APP_NAME=MyApp
|
||||||
|
`;
|
||||||
|
|
||||||
|
const serviceEnv = `
|
||||||
|
NODE_ENV='\${{project.ENVIRONMENT}}'
|
||||||
|
COMPLEX_VAR='Prefix-$#^!@-\${{project.ENVIRONMENT}}--\${{project.APP_NAME}} Suffix'
|
||||||
|
`;
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"NODE_ENV=STAGING",
|
||||||
|
"COMPLEX_VAR=Prefix-$#^!@-STAGING--MyApp Suffix",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles double and single quotes combined", () => {
|
||||||
|
const projectEnv = `
|
||||||
|
ENVIRONMENT=PRODUCTION
|
||||||
|
APP_NAME=MyApp
|
||||||
|
`;
|
||||||
|
const serviceEnv = `
|
||||||
|
NODE_ENV="'\${{project.ENVIRONMENT}}'"
|
||||||
|
COMPLEX_VAR="'Prefix \"DoubleQuoted\" and \${{project.APP_NAME}}'"
|
||||||
|
`;
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"NODE_ENV='PRODUCTION'",
|
||||||
|
"COMPLEX_VAR='Prefix \"DoubleQuoted\" and MyApp'",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -12,7 +12,16 @@ const baseApp: ApplicationNested = {
|
|||||||
serverId: "",
|
serverId: "",
|
||||||
branch: null,
|
branch: null,
|
||||||
dockerBuildStage: "",
|
dockerBuildStage: "",
|
||||||
|
registryUrl: "",
|
||||||
buildArgs: null,
|
buildArgs: null,
|
||||||
|
project: {
|
||||||
|
env: "",
|
||||||
|
adminId: "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
createdAt: "",
|
||||||
|
projectId: "",
|
||||||
|
},
|
||||||
buildPath: "/",
|
buildPath: "/",
|
||||||
gitlabPathNamespace: "",
|
gitlabPathNamespace: "",
|
||||||
buildType: "nixpacks",
|
buildType: "nixpacks",
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export const UpdatePort = ({ portId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Target Port</FormLabel>
|
<FormLabel>Target Port</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="1-65535" {...field} />
|
<NumberInput placeholder="1-65535" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ const redirectPresets = [
|
|||||||
redirect: {
|
redirect: {
|
||||||
regex: "^https?://(?:www.)?(.+)",
|
regex: "^https?://(?:www.)?(.+)",
|
||||||
permanent: true,
|
permanent: true,
|
||||||
replacement: "https://www.$${1}",
|
replacement: "https://www.${1}",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -70,7 +70,7 @@ const redirectPresets = [
|
|||||||
redirect: {
|
redirect: {
|
||||||
regex: "^https?://www.(.+)",
|
regex: "^https?://www.(.+)",
|
||||||
permanent: true,
|
permanent: true,
|
||||||
replacement: "https://$${1}",
|
replacement: "https://${1}",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export const UpdateVolume = ({
|
|||||||
} else if (typeForm === "file") {
|
} else if (typeForm === "file") {
|
||||||
form.reset({
|
form.reset({
|
||||||
content: data.content || "",
|
content: data.content || "",
|
||||||
mountPath: data.mountPath,
|
mountPath: "/",
|
||||||
filePath: data.filePath || "",
|
filePath: data.filePath || "",
|
||||||
type: "file",
|
type: "file",
|
||||||
});
|
});
|
||||||
@@ -296,15 +296,13 @@ export const UpdateVolume = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose>
|
<Button
|
||||||
<Button
|
isLoading={isLoading}
|
||||||
isLoading={isLoading}
|
// form="hook-form-update-volume"
|
||||||
form="hook-form-update-volume"
|
type="submit"
|
||||||
type="submit"
|
>
|
||||||
>
|
Update
|
||||||
Update
|
</Button>
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1,63 +1,143 @@
|
|||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { TrashIcon } from "lucide-react";
|
import { TrashIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const deleteApplicationSchema = z.object({
|
||||||
|
projectName: z.string().min(1, {
|
||||||
|
message: "Application name is required",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type DeleteApplication = z.infer<typeof deleteApplicationSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteApplication = ({ applicationId }: Props) => {
|
export const DeleteApplication = ({ applicationId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { mutateAsync, isLoading } = api.application.delete.useMutation();
|
const { mutateAsync, isLoading } = api.application.delete.useMutation();
|
||||||
|
const { data } = api.application.one.useQuery(
|
||||||
|
{ applicationId },
|
||||||
|
{ enabled: !!applicationId },
|
||||||
|
);
|
||||||
const { push } = useRouter();
|
const { push } = useRouter();
|
||||||
|
const form = useForm<DeleteApplication>({
|
||||||
|
defaultValues: {
|
||||||
|
projectName: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(deleteApplicationSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (formData: DeleteApplication) => {
|
||||||
|
const expectedName = `${data?.name}/${data?.appName}`;
|
||||||
|
if (formData.projectName === expectedName) {
|
||||||
|
await mutateAsync({
|
||||||
|
applicationId,
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
push(`/dashboard/project/${data?.projectId}`);
|
||||||
|
toast.success("Application deleted successfully");
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deleting the application");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
form.setError("projectName", {
|
||||||
|
message: "Project name does not match",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<AlertDialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<TrashIcon className="size-4 text-muted-foreground" />
|
<TrashIcon className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</DialogTrigger>
|
||||||
<AlertDialogContent>
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
<AlertDialogHeader>
|
<DialogHeader>
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||||
<AlertDialogDescription>
|
<DialogDescription>
|
||||||
This action cannot be undone. This will permanently delete the
|
This action cannot be undone. This will permanently delete the
|
||||||
application
|
application. If you are sure please enter the application name to
|
||||||
</AlertDialogDescription>
|
delete this application.
|
||||||
</AlertDialogHeader>
|
</DialogDescription>
|
||||||
<AlertDialogFooter>
|
</DialogHeader>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<div className="grid gap-4">
|
||||||
<AlertDialogAction
|
<Form {...form}>
|
||||||
onClick={async () => {
|
<form
|
||||||
await mutateAsync({
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
applicationId,
|
id="hook-form-delete-application"
|
||||||
})
|
className="grid w-full gap-4"
|
||||||
.then((data) => {
|
>
|
||||||
push(`/dashboard/project/${data?.projectId}`);
|
<FormField
|
||||||
|
control={form.control}
|
||||||
toast.success("Application delete succesfully");
|
name="projectName"
|
||||||
})
|
render={({ field }) => (
|
||||||
.catch(() => {
|
<FormItem>
|
||||||
toast.error("Error to delete Application");
|
<FormLabel>
|
||||||
});
|
To confirm, type "{data?.name}/{data?.appName}" in the box
|
||||||
|
below
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter application name to confirm"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
form="hook-form-delete-application"
|
||||||
|
type="submit"
|
||||||
|
variant="destructive"
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
</AlertDialogAction>
|
</Button>
|
||||||
</AlertDialogFooter>
|
</DialogFooter>
|
||||||
</AlertDialogContent>
|
</DialogContent>
|
||||||
</AlertDialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -202,7 +202,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full justify-between !bg-input",
|
"w-full justify-between !bg-input",
|
||||||
!field.value && "text-muted-foreground",
|
!field.value && "text-muted-foreground",
|
||||||
@@ -281,7 +280,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
" w-full justify-between !bg-input",
|
" w-full justify-between !bg-input",
|
||||||
!field.value && "text-muted-foreground",
|
!field.value && "text-muted-foreground",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const DockerProviderSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
username: z.string().optional(),
|
username: z.string().optional(),
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
|
registryURL: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
@@ -33,12 +34,12 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
|||||||
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
||||||
|
|
||||||
const { mutateAsync } = api.application.saveDockerProvider.useMutation();
|
const { mutateAsync } = api.application.saveDockerProvider.useMutation();
|
||||||
|
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
dockerImage: "",
|
dockerImage: "",
|
||||||
password: "",
|
password: "",
|
||||||
username: "",
|
username: "",
|
||||||
|
registryURL: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
});
|
});
|
||||||
@@ -49,6 +50,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
|||||||
dockerImage: data.dockerImage || "",
|
dockerImage: data.dockerImage || "",
|
||||||
password: data.password || "",
|
password: data.password || "",
|
||||||
username: data.username || "",
|
username: data.username || "",
|
||||||
|
registryURL: data.registryUrl || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
@@ -59,6 +61,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
|||||||
password: values.password || null,
|
password: values.password || null,
|
||||||
applicationId,
|
applicationId,
|
||||||
username: values.username || null,
|
username: values.username || null,
|
||||||
|
registryUrl: values.registryURL || null,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Docker Provider Saved");
|
toast.success("Docker Provider Saved");
|
||||||
@@ -76,7 +79,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
|||||||
className="flex flex-col gap-4"
|
className="flex flex-col gap-4"
|
||||||
>
|
>
|
||||||
<div className="grid md:grid-cols-2 gap-4 ">
|
<div className="grid md:grid-cols-2 gap-4 ">
|
||||||
<div className="md:col-span-2 space-y-4">
|
<div className="space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="dockerImage"
|
name="dockerImage"
|
||||||
@@ -91,6 +94,19 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="registryURL"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Registry URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Registry URL" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -193,7 +193,6 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full justify-between !bg-input",
|
"w-full justify-between !bg-input",
|
||||||
!field.value && "text-muted-foreground",
|
!field.value && "text-muted-foreground",
|
||||||
@@ -272,7 +271,6 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
" w-full justify-between !bg-input",
|
" w-full justify-between !bg-input",
|
||||||
!field.value && "text-muted-foreground",
|
!field.value && "text-muted-foreground",
|
||||||
|
|||||||
@@ -209,7 +209,6 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full justify-between !bg-input",
|
"w-full justify-between !bg-input",
|
||||||
!field.value && "text-muted-foreground",
|
!field.value && "text-muted-foreground",
|
||||||
@@ -297,7 +296,6 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
" w-full justify-between !bg-input",
|
" w-full justify-between !bg-input",
|
||||||
!field.value && "text-muted-foreground",
|
!field.value && "text-muted-foreground",
|
||||||
|
|||||||
@@ -1,63 +1,141 @@
|
|||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { TrashIcon } from "lucide-react";
|
import { TrashIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const deleteComposeSchema = z.object({
|
||||||
|
projectName: z.string().min(1, {
|
||||||
|
message: "Compose name is required",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type DeleteCompose = z.infer<typeof deleteComposeSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
composeId: string;
|
composeId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteCompose = ({ composeId }: Props) => {
|
export const DeleteCompose = ({ composeId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { mutateAsync, isLoading } = api.compose.delete.useMutation();
|
const { mutateAsync, isLoading } = api.compose.delete.useMutation();
|
||||||
|
const { data } = api.compose.one.useQuery(
|
||||||
|
{ composeId },
|
||||||
|
{ enabled: !!composeId },
|
||||||
|
);
|
||||||
const { push } = useRouter();
|
const { push } = useRouter();
|
||||||
|
const form = useForm<DeleteCompose>({
|
||||||
|
defaultValues: {
|
||||||
|
projectName: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(deleteComposeSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (formData: DeleteCompose) => {
|
||||||
|
const expectedName = `${data?.name}/${data?.appName}`;
|
||||||
|
if (formData.projectName === expectedName) {
|
||||||
|
await mutateAsync({ composeId })
|
||||||
|
.then((result) => {
|
||||||
|
push(`/dashboard/project/${result?.projectId}`);
|
||||||
|
toast.success("Compose deleted successfully");
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deleting the compose");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
form.setError("projectName", {
|
||||||
|
message: `Project name must match "${expectedName}"`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<AlertDialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<TrashIcon className="size-4 text-muted-foreground" />
|
<TrashIcon className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</DialogTrigger>
|
||||||
<AlertDialogContent>
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
<AlertDialogHeader>
|
<DialogHeader>
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||||
<AlertDialogDescription>
|
<DialogDescription>
|
||||||
This action cannot be undone. This will permanently delete the
|
This action cannot be undone. This will permanently delete the
|
||||||
compose and all its services.
|
compose. If you are sure please enter the compose name to delete
|
||||||
</AlertDialogDescription>
|
this compose.
|
||||||
</AlertDialogHeader>
|
</DialogDescription>
|
||||||
<AlertDialogFooter>
|
</DialogHeader>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<div className="grid gap-4">
|
||||||
<AlertDialogAction
|
<Form {...form}>
|
||||||
onClick={async () => {
|
<form
|
||||||
await mutateAsync({
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
composeId,
|
id="hook-form-delete-compose"
|
||||||
})
|
className="grid w-full gap-4"
|
||||||
.then((data) => {
|
>
|
||||||
push(`/dashboard/project/${data?.projectId}`);
|
<FormField
|
||||||
|
control={form.control}
|
||||||
toast.success("Compose delete succesfully");
|
name="projectName"
|
||||||
})
|
render={({ field }) => (
|
||||||
.catch(() => {
|
<FormItem>
|
||||||
toast.error("Error to delete the compose");
|
<FormLabel>
|
||||||
});
|
To confirm, type "{data?.name}/{data?.appName}" in the box
|
||||||
|
below
|
||||||
|
</FormLabel>{" "}
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter compose name to confirm"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
form="hook-form-delete-compose"
|
||||||
|
type="submit"
|
||||||
|
variant="destructive"
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
</AlertDialogAction>
|
</Button>
|
||||||
</AlertDialogFooter>
|
</DialogFooter>
|
||||||
</AlertDialogContent>
|
</DialogContent>
|
||||||
</AlertDialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -204,7 +204,6 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full justify-between !bg-input",
|
"w-full justify-between !bg-input",
|
||||||
!field.value && "text-muted-foreground",
|
!field.value && "text-muted-foreground",
|
||||||
@@ -283,7 +282,6 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
" w-full justify-between !bg-input",
|
" w-full justify-between !bg-input",
|
||||||
!field.value && "text-muted-foreground",
|
!field.value && "text-muted-foreground",
|
||||||
|
|||||||
@@ -195,7 +195,6 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full justify-between !bg-input",
|
"w-full justify-between !bg-input",
|
||||||
!field.value && "text-muted-foreground",
|
!field.value && "text-muted-foreground",
|
||||||
@@ -274,7 +273,6 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
" w-full justify-between !bg-input",
|
" w-full justify-between !bg-input",
|
||||||
!field.value && "text-muted-foreground",
|
!field.value && "text-muted-foreground",
|
||||||
|
|||||||
@@ -211,7 +211,6 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full justify-between !bg-input",
|
"w-full justify-between !bg-input",
|
||||||
!field.value && "text-muted-foreground",
|
!field.value && "text-muted-foreground",
|
||||||
@@ -299,7 +298,6 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
" w-full justify-between !bg-input",
|
" w-full justify-between !bg-input",
|
||||||
!field.value && "text-muted-foreground",
|
!field.value && "text-muted-foreground",
|
||||||
|
|||||||
@@ -160,7 +160,6 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full justify-between !bg-input",
|
"w-full justify-between !bg-input",
|
||||||
!field.value && "text-muted-foreground",
|
!field.value && "text-muted-foreground",
|
||||||
|
|||||||
@@ -144,7 +144,6 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full justify-between !bg-input",
|
"w-full justify-between !bg-input",
|
||||||
!field.value && "text-muted-foreground",
|
!field.value && "text-muted-foreground",
|
||||||
|
|||||||
@@ -1,62 +1,140 @@
|
|||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { TrashIcon } from "lucide-react";
|
import { TrashIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const deleteMariadbSchema = z.object({
|
||||||
|
projectName: z.string().min(1, {
|
||||||
|
message: "Database name is required",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type DeleteMariadb = z.infer<typeof deleteMariadbSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mariadbId: string;
|
mariadbId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteMariadb = ({ mariadbId }: Props) => {
|
export const DeleteMariadb = ({ mariadbId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { mutateAsync, isLoading } = api.mariadb.remove.useMutation();
|
const { mutateAsync, isLoading } = api.mariadb.remove.useMutation();
|
||||||
|
const { data } = api.mariadb.one.useQuery(
|
||||||
|
{ mariadbId },
|
||||||
|
{ enabled: !!mariadbId },
|
||||||
|
);
|
||||||
const { push } = useRouter();
|
const { push } = useRouter();
|
||||||
|
const form = useForm<DeleteMariadb>({
|
||||||
|
defaultValues: {
|
||||||
|
projectName: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(deleteMariadbSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (formData: DeleteMariadb) => {
|
||||||
|
const expectedName = `${data?.name}/${data?.appName}`;
|
||||||
|
if (formData.projectName === expectedName) {
|
||||||
|
await mutateAsync({ mariadbId })
|
||||||
|
.then((data) => {
|
||||||
|
push(`/dashboard/project/${data?.projectId}`);
|
||||||
|
toast.success("Database deleted successfully");
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deleting the database");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
form.setError("projectName", {
|
||||||
|
message: "Database name does not match",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<AlertDialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<TrashIcon className="size-4 text-muted-foreground " />
|
<TrashIcon className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</DialogTrigger>
|
||||||
<AlertDialogContent>
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
<AlertDialogHeader>
|
<DialogHeader>
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||||
<AlertDialogDescription>
|
<DialogDescription>
|
||||||
This action cannot be undone. This will permanently delete the
|
This action cannot be undone. This will permanently delete the
|
||||||
database
|
database. If you are sure please enter the database name to delete
|
||||||
</AlertDialogDescription>
|
this database.
|
||||||
</AlertDialogHeader>
|
</DialogDescription>
|
||||||
<AlertDialogFooter>
|
</DialogHeader>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<div className="grid gap-4">
|
||||||
<AlertDialogAction
|
<Form {...form}>
|
||||||
onClick={async () => {
|
<form
|
||||||
await mutateAsync({
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
mariadbId,
|
id="hook-form-delete-mariadb"
|
||||||
})
|
className="grid w-full gap-4"
|
||||||
.then((data) => {
|
>
|
||||||
push(`/dashboard/project/${data?.projectId}`);
|
<FormField
|
||||||
toast.success("Database delete succesfully");
|
control={form.control}
|
||||||
})
|
name="projectName"
|
||||||
.catch(() => {
|
render={({ field }) => (
|
||||||
toast.error("Error to delete the database");
|
<FormItem>
|
||||||
});
|
<FormLabel>
|
||||||
|
To confirm, type "{data?.name}/{data?.appName}" in the box
|
||||||
|
below
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter database name to confirm"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
form="hook-form-delete-mariadb"
|
||||||
|
type="submit"
|
||||||
|
variant="destructive"
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
</AlertDialogAction>
|
</Button>
|
||||||
</AlertDialogFooter>
|
</DialogFooter>
|
||||||
</AlertDialogContent>
|
</DialogContent>
|
||||||
</AlertDialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,62 +1,139 @@
|
|||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { TrashIcon } from "lucide-react";
|
import { TrashIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const deleteMongoSchema = z.object({
|
||||||
|
projectName: z.string().min(1, {
|
||||||
|
message: "Database name is required",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type DeleteMongo = z.infer<typeof deleteMongoSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mongoId: string;
|
mongoId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// commen
|
||||||
|
|
||||||
export const DeleteMongo = ({ mongoId }: Props) => {
|
export const DeleteMongo = ({ mongoId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { mutateAsync, isLoading } = api.mongo.remove.useMutation();
|
const { mutateAsync, isLoading } = api.mongo.remove.useMutation();
|
||||||
|
const { data } = api.mongo.one.useQuery({ mongoId }, { enabled: !!mongoId });
|
||||||
const { push } = useRouter();
|
const { push } = useRouter();
|
||||||
|
const form = useForm<DeleteMongo>({
|
||||||
|
defaultValues: {
|
||||||
|
projectName: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(deleteMongoSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (formData: DeleteMongo) => {
|
||||||
|
const expectedName = `${data?.name}/${data?.appName}`;
|
||||||
|
if (formData.projectName === expectedName) {
|
||||||
|
await mutateAsync({ mongoId })
|
||||||
|
.then((data) => {
|
||||||
|
push(`/dashboard/project/${data?.projectId}`);
|
||||||
|
toast.success("Database deleted successfully");
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deleting the database");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
form.setError("projectName", {
|
||||||
|
message: "Database name does not match",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<AlertDialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<TrashIcon className="size-4 text-muted-foreground " />
|
<TrashIcon className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</DialogTrigger>
|
||||||
<AlertDialogContent>
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
<AlertDialogHeader>
|
<DialogHeader>
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||||
<AlertDialogDescription>
|
<DialogDescription>
|
||||||
This action cannot be undone. This will permanently delete the
|
This action cannot be undone. This will permanently delete the
|
||||||
database
|
database. If you are sure please enter the database name to delete
|
||||||
</AlertDialogDescription>
|
this database.
|
||||||
</AlertDialogHeader>
|
</DialogDescription>
|
||||||
<AlertDialogFooter>
|
</DialogHeader>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<div className="grid gap-4">
|
||||||
<AlertDialogAction
|
<Form {...form}>
|
||||||
onClick={async () => {
|
<form
|
||||||
await mutateAsync({
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
mongoId,
|
id="hook-form-delete-mongo"
|
||||||
})
|
className="grid w-full gap-4"
|
||||||
.then((data) => {
|
>
|
||||||
push(`/dashboard/project/${data?.projectId}`);
|
<FormField
|
||||||
toast.success("Database delete succesfully");
|
control={form.control}
|
||||||
})
|
name="projectName"
|
||||||
.catch(() => {
|
render={({ field }) => (
|
||||||
toast.error("Error to delete the database");
|
<FormItem>
|
||||||
});
|
<FormLabel>
|
||||||
|
To confirm, type "{data?.name}/{data?.appName}" in the box
|
||||||
|
below
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter database name to confirm"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
form="hook-form-delete-mongo"
|
||||||
|
type="submit"
|
||||||
|
variant="destructive"
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
</AlertDialogAction>
|
</Button>
|
||||||
</AlertDialogFooter>
|
</DialogFooter>
|
||||||
</AlertDialogContent>
|
</DialogContent>
|
||||||
</AlertDialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,62 +1,138 @@
|
|||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { TrashIcon } from "lucide-react";
|
import { TrashIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const deleteMysqlSchema = z.object({
|
||||||
|
projectName: z.string().min(1, {
|
||||||
|
message: "Database name is required",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type DeleteMysql = z.infer<typeof deleteMysqlSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mysqlId: string;
|
mysqlId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteMysql = ({ mysqlId }: Props) => {
|
export const DeleteMysql = ({ mysqlId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { mutateAsync, isLoading } = api.mysql.remove.useMutation();
|
const { mutateAsync, isLoading } = api.mysql.remove.useMutation();
|
||||||
|
const { data } = api.mysql.one.useQuery({ mysqlId }, { enabled: !!mysqlId });
|
||||||
const { push } = useRouter();
|
const { push } = useRouter();
|
||||||
|
const form = useForm<DeleteMysql>({
|
||||||
|
defaultValues: {
|
||||||
|
projectName: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(deleteMysqlSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (formData: DeleteMysql) => {
|
||||||
|
const expectedName = `${data?.name}/${data?.appName}`;
|
||||||
|
if (formData.projectName === expectedName) {
|
||||||
|
await mutateAsync({ mysqlId })
|
||||||
|
.then((data) => {
|
||||||
|
push(`/dashboard/project/${data?.projectId}`);
|
||||||
|
toast.success("Database deleted successfully");
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deleting the database");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
form.setError("projectName", {
|
||||||
|
message: "Database name does not match",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<AlertDialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<TrashIcon className="size-4 text-muted-foreground " />
|
<TrashIcon className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</DialogTrigger>
|
||||||
<AlertDialogContent>
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
<AlertDialogHeader>
|
<DialogHeader>
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||||
<AlertDialogDescription>
|
<DialogDescription>
|
||||||
This action cannot be undone. This will permanently delete the
|
This action cannot be undone. This will permanently delete the
|
||||||
database
|
database. If you are sure please enter the database name to delete
|
||||||
</AlertDialogDescription>
|
this database.
|
||||||
</AlertDialogHeader>
|
</DialogDescription>
|
||||||
<AlertDialogFooter>
|
</DialogHeader>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<div className="grid gap-4">
|
||||||
<AlertDialogAction
|
<Form {...form}>
|
||||||
onClick={async () => {
|
<form
|
||||||
await mutateAsync({
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
mysqlId,
|
id="hook-form-delete-mysql"
|
||||||
})
|
className="grid w-full gap-4"
|
||||||
.then((data) => {
|
>
|
||||||
push(`/dashboard/project/${data?.projectId}`);
|
<FormField
|
||||||
toast.success("Database delete succesfully");
|
control={form.control}
|
||||||
})
|
name="projectName"
|
||||||
.catch(() => {
|
render={({ field }) => (
|
||||||
toast.error("Error to delete the database");
|
<FormItem>
|
||||||
});
|
<FormLabel>
|
||||||
|
To confirm, type "{data?.name}/{data?.appName}" in the box
|
||||||
|
below
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter database name to confirm"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
form="hook-form-delete-mysql"
|
||||||
|
type="submit"
|
||||||
|
variant="destructive"
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
</AlertDialogAction>
|
</Button>
|
||||||
</AlertDialogFooter>
|
</DialogFooter>
|
||||||
</AlertDialogContent>
|
</DialogContent>
|
||||||
</AlertDialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -86,14 +86,12 @@ export const ShowVolumes = ({ mysqlId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{mount.type === "file" && (
|
{mount.type === "file" && (
|
||||||
<>
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex flex-col gap-1">
|
<span className="font-medium">Content</span>
|
||||||
<span className="font-medium">Content</span>
|
<span className="text-sm text-muted-foreground">
|
||||||
<span className="text-sm text-muted-foreground">
|
{mount.content}
|
||||||
{mount.content}
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{mount.type === "bind" && (
|
{mount.type === "bind" && (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
|
|||||||
@@ -1,62 +1,141 @@
|
|||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { TrashIcon } from "lucide-react";
|
import { TrashIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const deletePostgresSchema = z.object({
|
||||||
|
projectName: z.string().min(1, {
|
||||||
|
message: "Database name is required",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type DeletePostgres = z.infer<typeof deletePostgresSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postgresId: string;
|
postgresId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeletePostgres = ({ postgresId }: Props) => {
|
export const DeletePostgres = ({ postgresId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { mutateAsync, isLoading } = api.postgres.remove.useMutation();
|
const { mutateAsync, isLoading } = api.postgres.remove.useMutation();
|
||||||
|
const { data } = api.postgres.one.useQuery(
|
||||||
|
{ postgresId },
|
||||||
|
{ enabled: !!postgresId },
|
||||||
|
);
|
||||||
const { push } = useRouter();
|
const { push } = useRouter();
|
||||||
|
const form = useForm<DeletePostgres>({
|
||||||
|
defaultValues: {
|
||||||
|
projectName: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(deletePostgresSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (formData: DeletePostgres) => {
|
||||||
|
const expectedName = `${data?.name}/${data?.appName}`;
|
||||||
|
if (formData.projectName === expectedName) {
|
||||||
|
await mutateAsync({ postgresId })
|
||||||
|
.then((data) => {
|
||||||
|
push(`/dashboard/project/${data?.projectId}`);
|
||||||
|
toast.success("Database deleted successfully");
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deleting the database");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
form.setError("projectName", {
|
||||||
|
message: "Database name does not match",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<AlertDialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<TrashIcon className="size-4 text-muted-foreground " />
|
<TrashIcon className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</DialogTrigger>
|
||||||
<AlertDialogContent>
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
<AlertDialogHeader>
|
<DialogHeader>
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||||
<AlertDialogDescription>
|
<DialogDescription>
|
||||||
This action cannot be undone. This will permanently delete the
|
This action cannot be undone. This will permanently delete the
|
||||||
database
|
database. If you are sure please enter the database name to delete
|
||||||
</AlertDialogDescription>
|
this database.
|
||||||
</AlertDialogHeader>
|
</DialogDescription>
|
||||||
<AlertDialogFooter>
|
</DialogHeader>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<div className="grid gap-4">
|
||||||
<AlertDialogAction
|
<Form {...form}>
|
||||||
onClick={async () => {
|
<form
|
||||||
await mutateAsync({
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
postgresId,
|
id="hook-form-delete-postgres"
|
||||||
})
|
className="grid w-full gap-4"
|
||||||
.then((data) => {
|
>
|
||||||
push(`/dashboard/project/${data?.projectId}`);
|
<FormField
|
||||||
toast.success("Database delete succesfully");
|
control={form.control}
|
||||||
})
|
name="projectName"
|
||||||
.catch(() => {
|
render={({ field }) => (
|
||||||
toast.error("Error to delete the database");
|
<FormItem>
|
||||||
});
|
<FormLabel>
|
||||||
|
To confirm, type "{data?.name}/{data?.appName}" in the box
|
||||||
|
below
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter database name to confirm"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
form="hook-form-delete-postgres"
|
||||||
|
type="submit"
|
||||||
|
variant="destructive"
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
</AlertDialogAction>
|
</Button>
|
||||||
</AlertDialogFooter>
|
</DialogFooter>
|
||||||
</AlertDialogContent>
|
</DialogContent>
|
||||||
</AlertDialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -127,7 +127,6 @@ export const AddTemplate = ({ projectId }: Props) => {
|
|||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"md:max-w-[15rem] w-full justify-between !bg-input",
|
"md:max-w-[15rem] w-full justify-between !bg-input",
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { FileIcon } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const updateProjectSchema = z.object({
|
||||||
|
env: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type UpdateProject = z.infer<typeof updateProjectSchema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
projectId: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProjectEnviroment = ({ projectId, children }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { mutateAsync, error, isError, isLoading } =
|
||||||
|
api.project.update.useMutation();
|
||||||
|
const { data } = api.project.one.useQuery(
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!projectId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const form = useForm<UpdateProject>({
|
||||||
|
defaultValues: {
|
||||||
|
env: data?.env ?? "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(updateProjectSchema),
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
env: data.env ?? "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data, form, form.reset]);
|
||||||
|
|
||||||
|
const onSubmit = async (formData: UpdateProject) => {
|
||||||
|
await mutateAsync({
|
||||||
|
env: formData.env || "",
|
||||||
|
projectId: projectId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Project env updated succesfully");
|
||||||
|
utils.project.all.invalidate();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to update the env");
|
||||||
|
})
|
||||||
|
.finally(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
{children ?? (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer space-x-3"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<FileIcon className="size-4" />
|
||||||
|
<span>Project Enviroment</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-6xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Project Enviroment</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Update the env Environment variables that are accessible to all
|
||||||
|
services of this project. Use this syntax to reference project-level
|
||||||
|
variables in your service environments:
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
<AlertBlock type="info">
|
||||||
|
Use this syntax to reference project-level variables in your service
|
||||||
|
environments: <code>DATABASE_URL=${"{{project.DATABASE_URL}}"}</code>
|
||||||
|
</AlertBlock>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid items-center gap-4">
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-4 "
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="env"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Enviroment variables</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<CodeEditor
|
||||||
|
lineWrapping
|
||||||
|
language="properties"
|
||||||
|
wrapperClassName="h-[35rem] font-mono"
|
||||||
|
placeholder={`NODE_ENV=production
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
`}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<FormMessage />
|
||||||
|
</pre>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button isLoading={isLoading} type="submit">
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -25,7 +25,6 @@ import { api } from "@/utils/api";
|
|||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
BookIcon,
|
BookIcon,
|
||||||
CircuitBoard,
|
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
FolderInput,
|
FolderInput,
|
||||||
@@ -35,6 +34,7 @@ import {
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { ProjectEnviroment } from "./project-enviroment";
|
||||||
import { UpdateProject } from "./update";
|
import { UpdateProject } from "./update";
|
||||||
|
|
||||||
export const ShowProjects = () => {
|
export const ShowProjects = () => {
|
||||||
@@ -190,7 +190,11 @@ export const ShowProjects = () => {
|
|||||||
<DropdownMenuLabel className="font-normal">
|
<DropdownMenuLabel className="font-normal">
|
||||||
Actions
|
Actions
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
|
<ProjectEnviroment
|
||||||
|
projectId={project.projectId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div onClick={(e) => e.stopPropagation()}>
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
<UpdateProject projectId={project.projectId} />
|
<UpdateProject projectId={project.projectId} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,62 +1,138 @@
|
|||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { TrashIcon } from "lucide-react";
|
import { TrashIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const deleteRedisSchema = z.object({
|
||||||
|
projectName: z.string().min(1, {
|
||||||
|
message: "Database name is required",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type DeleteRedis = z.infer<typeof deleteRedisSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
redisId: string;
|
redisId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteRedis = ({ redisId }: Props) => {
|
export const DeleteRedis = ({ redisId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { mutateAsync, isLoading } = api.redis.remove.useMutation();
|
const { mutateAsync, isLoading } = api.redis.remove.useMutation();
|
||||||
|
const { data } = api.redis.one.useQuery({ redisId }, { enabled: !!redisId });
|
||||||
const { push } = useRouter();
|
const { push } = useRouter();
|
||||||
|
const form = useForm<DeleteRedis>({
|
||||||
|
defaultValues: {
|
||||||
|
projectName: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(deleteRedisSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (formData: DeleteRedis) => {
|
||||||
|
const expectedName = `${data?.name}/${data?.appName}`;
|
||||||
|
if (formData.projectName === expectedName) {
|
||||||
|
await mutateAsync({ redisId })
|
||||||
|
.then((data) => {
|
||||||
|
push(`/dashboard/project/${data?.projectId}`);
|
||||||
|
toast.success("Database deleted successfully");
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deleting the database");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
form.setError("projectName", {
|
||||||
|
message: "Database name does not match",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<AlertDialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<TrashIcon className="size-4 text-muted-foreground " />
|
<TrashIcon className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</DialogTrigger>
|
||||||
<AlertDialogContent>
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
<AlertDialogHeader>
|
<DialogHeader>
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||||
<AlertDialogDescription>
|
<DialogDescription>
|
||||||
This action cannot be undone. This will permanently delete the
|
This action cannot be undone. This will permanently delete the
|
||||||
database
|
database. If you are sure please enter the database name to delete
|
||||||
</AlertDialogDescription>
|
this database.
|
||||||
</AlertDialogHeader>
|
</DialogDescription>
|
||||||
<AlertDialogFooter>
|
</DialogHeader>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<div className="grid gap-4">
|
||||||
<AlertDialogAction
|
<Form {...form}>
|
||||||
onClick={async () => {
|
<form
|
||||||
await mutateAsync({
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
redisId,
|
id="hook-form-delete-redis"
|
||||||
})
|
className="grid w-full gap-4"
|
||||||
.then((data) => {
|
>
|
||||||
push(`/dashboard/project/${data?.projectId}`);
|
<FormField
|
||||||
toast.success("Database delete succesfully");
|
control={form.control}
|
||||||
})
|
name="projectName"
|
||||||
.catch(() => {
|
render={({ field }) => (
|
||||||
toast.error("Error to delete the database");
|
<FormItem>
|
||||||
});
|
<FormLabel>
|
||||||
|
To confirm, type "{data?.name}/{data?.appName}" in the box
|
||||||
|
below
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter database name to confirm"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
form="hook-form-delete-redis"
|
||||||
|
type="submit"
|
||||||
|
variant="destructive"
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
</AlertDialogAction>
|
</Button>
|
||||||
</AlertDialogFooter>
|
</DialogFooter>
|
||||||
</AlertDialogContent>
|
</DialogContent>
|
||||||
</AlertDialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,15 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import useLocale from "@/utils/hooks/use-locale";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -28,6 +37,12 @@ const appearanceFormSchema = z.object({
|
|||||||
theme: z.enum(["light", "dark", "system"], {
|
theme: z.enum(["light", "dark", "system"], {
|
||||||
required_error: "Please select a theme.",
|
required_error: "Please select a theme.",
|
||||||
}),
|
}),
|
||||||
|
language: z.enum(
|
||||||
|
["en", "pl", "ru", "fr", "de", "tr", "zh-Hant", "zh-Hans", "fa"],
|
||||||
|
{
|
||||||
|
required_error: "Please select a language.",
|
||||||
|
},
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AppearanceFormValues = z.infer<typeof appearanceFormSchema>;
|
type AppearanceFormValues = z.infer<typeof appearanceFormSchema>;
|
||||||
@@ -35,10 +50,14 @@ type AppearanceFormValues = z.infer<typeof appearanceFormSchema>;
|
|||||||
// This can come from your database or API.
|
// This can come from your database or API.
|
||||||
const defaultValues: Partial<AppearanceFormValues> = {
|
const defaultValues: Partial<AppearanceFormValues> = {
|
||||||
theme: "system",
|
theme: "system",
|
||||||
|
language: "en",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AppearanceForm() {
|
export function AppearanceForm() {
|
||||||
const { setTheme, theme } = useTheme();
|
const { setTheme, theme } = useTheme();
|
||||||
|
const { locale, setLocale } = useLocale();
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
|
|
||||||
const form = useForm<AppearanceFormValues>({
|
const form = useForm<AppearanceFormValues>({
|
||||||
resolver: zodResolver(appearanceFormSchema),
|
resolver: zodResolver(appearanceFormSchema),
|
||||||
defaultValues,
|
defaultValues,
|
||||||
@@ -47,19 +66,23 @@ export function AppearanceForm() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
theme: (theme ?? "system") as AppearanceFormValues["theme"],
|
theme: (theme ?? "system") as AppearanceFormValues["theme"],
|
||||||
|
language: locale,
|
||||||
});
|
});
|
||||||
}, [form, theme]);
|
}, [form, theme, locale]);
|
||||||
function onSubmit(data: AppearanceFormValues) {
|
function onSubmit(data: AppearanceFormValues) {
|
||||||
setTheme(data.theme);
|
setTheme(data.theme);
|
||||||
|
setLocale(data.language);
|
||||||
toast.success("Preferences Updated");
|
toast.success("Preferences Updated");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-transparent">
|
<Card className="bg-transparent">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Appearance</CardTitle>
|
<CardTitle className="text-xl">
|
||||||
|
{t("settings.appearance.title")}
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Customize the theme of your dashboard.
|
{t("settings.appearance.description")}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2">
|
<CardContent className="space-y-2">
|
||||||
@@ -72,9 +95,9 @@ export function AppearanceForm() {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem className="space-y-1 ">
|
<FormItem className="space-y-1 ">
|
||||||
<FormLabel>Theme</FormLabel>
|
<FormLabel>{t("settings.appearance.theme")}</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Select a theme for your dashboard
|
{t("settings.appearance.themeDescription")}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
@@ -92,7 +115,7 @@ export function AppearanceForm() {
|
|||||||
<img src="/images/theme-light.svg" alt="light" />
|
<img src="/images/theme-light.svg" alt="light" />
|
||||||
</div>
|
</div>
|
||||||
<span className="block w-full p-2 text-center font-normal">
|
<span className="block w-full p-2 text-center font-normal">
|
||||||
Light
|
{t("settings.appearance.themes.light")}
|
||||||
</span>
|
</span>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -105,7 +128,7 @@ export function AppearanceForm() {
|
|||||||
<img src="/images/theme-dark.svg" alt="dark" />
|
<img src="/images/theme-dark.svg" alt="dark" />
|
||||||
</div>
|
</div>
|
||||||
<span className="block w-full p-2 text-center font-normal">
|
<span className="block w-full p-2 text-center font-normal">
|
||||||
Dark
|
{t("settings.appearance.themes.dark")}
|
||||||
</span>
|
</span>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -121,7 +144,7 @@ export function AppearanceForm() {
|
|||||||
<img src="/images/theme-system.svg" alt="system" />
|
<img src="/images/theme-system.svg" alt="system" />
|
||||||
</div>
|
</div>
|
||||||
<span className="block w-full p-2 text-center font-normal">
|
<span className="block w-full p-2 text-center font-normal">
|
||||||
System
|
{t("settings.appearance.themes.system")}
|
||||||
</span>
|
</span>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -131,7 +154,53 @@ export function AppearanceForm() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button type="submit">Save</Button>
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="language"
|
||||||
|
defaultValue={form.control._defaultValues.language}
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem className="space-y-1">
|
||||||
|
<FormLabel>{t("settings.appearance.language")}</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{t("settings.appearance.languageDescription")}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
value={field.value}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="No preset selected" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{[
|
||||||
|
{ label: "English", value: "en" },
|
||||||
|
{ label: "Polski", value: "pl" },
|
||||||
|
{ label: "Русский", value: "ru" },
|
||||||
|
{ label: "Français", value: "fr" },
|
||||||
|
{ label: "Deutsch", value: "de" },
|
||||||
|
{ label: "繁體中文", value: "zh-Hant" },
|
||||||
|
{ label: "简体中文", value: "zh-Hans" },
|
||||||
|
{ label: "Türkçe", value: "tr" },
|
||||||
|
{
|
||||||
|
label: "Persian",
|
||||||
|
value: "fa",
|
||||||
|
},
|
||||||
|
].map((preset) => (
|
||||||
|
<SelectItem key={preset.label} value={preset.value}>
|
||||||
|
{preset.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit">{t("settings.common.save")}</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -89,18 +89,14 @@ export const ShowBilling = () => {
|
|||||||
<div className="pb-5">
|
<div className="pb-5">
|
||||||
<Progress value={safePercentage} className="max-w-lg" />
|
<Progress value={safePercentage} className="max-w-lg" />
|
||||||
</div>
|
</div>
|
||||||
{admin && (
|
{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">
|
||||||
{admin.serversQuantity! <= servers?.length! && (
|
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
|
||||||
<div className="flex flex-row gap-4 p-2 bg-yellow-50 dark:bg-yellow-950 rounded-lg items-center">
|
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||||
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
|
You have reached the maximum number of servers you can create,
|
||||||
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
please upgrade your plan to add more servers.
|
||||||
You have reached the maximum number of servers you can
|
</span>
|
||||||
create, please upgrade your plan to add more servers.
|
</div>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -188,7 +184,6 @@ export const ShowBilling = () => {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul
|
<ul
|
||||||
role="list"
|
|
||||||
className={clsx(
|
className={clsx(
|
||||||
" mt-4 flex flex-col gap-y-2 text-sm",
|
" mt-4 flex flex-col gap-y-2 text-sm",
|
||||||
featured ? "text-white" : "text-slate-200",
|
featured ? "text-white" : "text-slate-200",
|
||||||
|
|||||||
@@ -28,11 +28,7 @@ export const ShowRegistry = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
{data && data?.length > 0 && (
|
{data && data?.length > 0 && <AddRegistry />}
|
||||||
<>
|
|
||||||
<AddRegistry />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2 pt-4 h-full">
|
<CardContent className="space-y-2 pt-4 h-full">
|
||||||
|
|||||||
@@ -34,9 +34,11 @@ import { useEffect } 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";
|
||||||
|
import { S3_PROVIDERS } from "./constants";
|
||||||
|
|
||||||
const addDestination = z.object({
|
const addDestination = z.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
|
provider: z.string().optional(),
|
||||||
accessKeyId: z.string(),
|
accessKeyId: z.string(),
|
||||||
secretAccessKey: z.string(),
|
secretAccessKey: z.string(),
|
||||||
bucket: z.string(),
|
bucket: z.string(),
|
||||||
@@ -58,6 +60,7 @@ export const AddDestination = () => {
|
|||||||
api.destination.testConnection.useMutation();
|
api.destination.testConnection.useMutation();
|
||||||
const form = useForm<AddDestination>({
|
const form = useForm<AddDestination>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
provider: "",
|
||||||
accessKeyId: "",
|
accessKeyId: "",
|
||||||
bucket: "",
|
bucket: "",
|
||||||
name: "",
|
name: "",
|
||||||
@@ -73,6 +76,7 @@ export const AddDestination = () => {
|
|||||||
|
|
||||||
const onSubmit = async (data: AddDestination) => {
|
const onSubmit = async (data: AddDestination) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
|
provider: data.provider || "",
|
||||||
accessKey: data.accessKeyId,
|
accessKey: data.accessKeyId,
|
||||||
bucket: data.bucket,
|
bucket: data.bucket,
|
||||||
endpoint: data.endpoint,
|
endpoint: data.endpoint,
|
||||||
@@ -123,6 +127,40 @@ export const AddDestination = () => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="provider"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Provider</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a S3 Provider" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{S3_PROVIDERS.map((s3Provider) => (
|
||||||
|
<SelectItem
|
||||||
|
key={s3Provider.key}
|
||||||
|
value={s3Provider.key}
|
||||||
|
>
|
||||||
|
{s3Provider.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -255,6 +293,7 @@ export const AddDestination = () => {
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await testConnection({
|
await testConnection({
|
||||||
|
provider: form.getValues("provider") || "",
|
||||||
accessKey: form.getValues("accessKeyId"),
|
accessKey: form.getValues("accessKeyId"),
|
||||||
bucket: form.getValues("bucket"),
|
bucket: form.getValues("bucket"),
|
||||||
endpoint: form.getValues("endpoint"),
|
endpoint: form.getValues("endpoint"),
|
||||||
@@ -283,6 +322,7 @@ export const AddDestination = () => {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await testConnection({
|
await testConnection({
|
||||||
|
provider: form.getValues("provider") || "",
|
||||||
accessKey: form.getValues("accessKeyId"),
|
accessKey: form.getValues("accessKeyId"),
|
||||||
bucket: form.getValues("bucket"),
|
bucket: form.getValues("bucket"),
|
||||||
endpoint: form.getValues("endpoint"),
|
endpoint: form.getValues("endpoint"),
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
export const S3_PROVIDERS: Array<{
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
key: "AWS",
|
||||||
|
name: "Amazon Web Services (AWS) S3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Alibaba",
|
||||||
|
name: "Alibaba Cloud Object Storage System (OSS) formerly Aliyun",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "ArvanCloud",
|
||||||
|
name: "Arvan Cloud Object Storage (AOS)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Ceph",
|
||||||
|
name: "Ceph Object Storage",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "ChinaMobile",
|
||||||
|
name: "China Mobile Ecloud Elastic Object Storage (EOS)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Cloudflare",
|
||||||
|
name: "Cloudflare R2 Storage",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "DigitalOcean",
|
||||||
|
name: "DigitalOcean Spaces",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Dreamhost",
|
||||||
|
name: "Dreamhost DreamObjects",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "GCS",
|
||||||
|
name: "Google Cloud Storage",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "HuaweiOBS",
|
||||||
|
name: "Huawei Object Storage Service",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "IBMCOS",
|
||||||
|
name: "IBM COS S3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "IDrive",
|
||||||
|
name: "IDrive e2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "IONOS",
|
||||||
|
name: "IONOS Cloud",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "LyveCloud",
|
||||||
|
name: "Seagate Lyve Cloud",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Leviia",
|
||||||
|
name: "Leviia Object Storage",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Liara",
|
||||||
|
name: "Liara Object Storage",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Linode",
|
||||||
|
name: "Linode Object Storage",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Magalu",
|
||||||
|
name: "Magalu Object Storage",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Minio",
|
||||||
|
name: "Minio Object Storage",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Netease",
|
||||||
|
name: "Netease Object Storage (NOS)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Petabox",
|
||||||
|
name: "Petabox Object Storage",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "RackCorp",
|
||||||
|
name: "RackCorp Object Storage",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Rclone",
|
||||||
|
name: "Rclone S3 Server",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Scaleway",
|
||||||
|
name: "Scaleway Object Storage",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "SeaweedFS",
|
||||||
|
name: "SeaweedFS S3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "StackPath",
|
||||||
|
name: "StackPath Object Storage",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Storj",
|
||||||
|
name: "Storj (S3 Compatible Gateway)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Synology",
|
||||||
|
name: "Synology C2 Object Storage",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "TencentCOS",
|
||||||
|
name: "Tencent Cloud Object Storage (COS)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Wasabi",
|
||||||
|
name: "Wasabi Object Storage",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Qiniu",
|
||||||
|
name: "Qiniu Object Storage (Kodo)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Other",
|
||||||
|
name: "Any other S3 compatible provider",
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -35,9 +35,11 @@ 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";
|
||||||
|
import { S3_PROVIDERS } from "./constants";
|
||||||
|
|
||||||
const updateDestination = z.object({
|
const updateDestination = z.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
|
provider: z.string().optional(),
|
||||||
accessKeyId: z.string(),
|
accessKeyId: z.string(),
|
||||||
secretAccessKey: z.string(),
|
secretAccessKey: z.string(),
|
||||||
bucket: z.string(),
|
bucket: z.string(),
|
||||||
@@ -70,6 +72,7 @@ export const UpdateDestination = ({ destinationId }: Props) => {
|
|||||||
api.destination.testConnection.useMutation();
|
api.destination.testConnection.useMutation();
|
||||||
const form = useForm<UpdateDestination>({
|
const form = useForm<UpdateDestination>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
provider: "",
|
||||||
accessKeyId: "",
|
accessKeyId: "",
|
||||||
bucket: "",
|
bucket: "",
|
||||||
name: "",
|
name: "",
|
||||||
@@ -152,6 +155,40 @@ export const UpdateDestination = ({ destinationId }: Props) => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="provider"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Provider</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a S3 Provider" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{S3_PROVIDERS.map((s3Provider) => (
|
||||||
|
<SelectItem
|
||||||
|
key={s3Provider.key}
|
||||||
|
value={s3Provider.key}
|
||||||
|
>
|
||||||
|
{s3Provider.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -285,6 +322,7 @@ export const UpdateDestination = ({ destinationId }: Props) => {
|
|||||||
variant={"secondary"}
|
variant={"secondary"}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await testConnection({
|
await testConnection({
|
||||||
|
provider: form.getValues("provider") || "",
|
||||||
accessKey: form.getValues("accessKeyId"),
|
accessKey: form.getValues("accessKeyId"),
|
||||||
bucket: form.getValues("bucket"),
|
bucket: form.getValues("bucket"),
|
||||||
endpoint: form.getValues("endpoint"),
|
endpoint: form.getValues("endpoint"),
|
||||||
@@ -311,6 +349,7 @@ export const UpdateDestination = ({ destinationId }: Props) => {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await testConnection({
|
await testConnection({
|
||||||
|
provider: form.getValues("provider") || "",
|
||||||
accessKey: form.getValues("accessKeyId"),
|
accessKey: form.getValues("accessKeyId"),
|
||||||
bucket: form.getValues("bucket"),
|
bucket: form.getValues("bucket"),
|
||||||
endpoint: form.getValues("endpoint"),
|
endpoint: form.getValues("endpoint"),
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const AddGithubProvider = () => {
|
|||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="secondary" className="flex items-center space-x-1">
|
<Button variant="secondary" className="flex items-center space-x-1">
|
||||||
<GithubIcon />
|
<GithubIcon className="text-current fill-current" />
|
||||||
<span>Github</span>
|
<span>Github</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|||||||
@@ -397,25 +397,23 @@ export const AddNotification = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{type === "discord" && (
|
{type === "discord" && (
|
||||||
<>
|
<FormField
|
||||||
<FormField
|
control={form.control}
|
||||||
control={form.control}
|
name="webhookUrl"
|
||||||
name="webhookUrl"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem>
|
||||||
<FormItem>
|
<FormLabel>Webhook URL</FormLabel>
|
||||||
<FormLabel>Webhook URL</FormLabel>
|
<FormControl>
|
||||||
<FormControl>
|
<Input
|
||||||
<Input
|
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
{...field}
|
||||||
{...field}
|
/>
|
||||||
/>
|
</FormControl>
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{type === "email" && (
|
{type === "email" && (
|
||||||
|
|||||||
@@ -356,25 +356,23 @@ export const UpdateNotification = ({ notificationId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{type === "discord" && (
|
{type === "discord" && (
|
||||||
<>
|
<FormField
|
||||||
<FormField
|
control={form.control}
|
||||||
control={form.control}
|
name="webhookUrl"
|
||||||
name="webhookUrl"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem>
|
||||||
<FormItem>
|
<FormLabel>Webhook URL</FormLabel>
|
||||||
<FormLabel>Webhook URL</FormLabel>
|
<FormControl>
|
||||||
<FormControl>
|
<Input
|
||||||
<Input
|
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
{...field}
|
||||||
{...field}
|
/>
|
||||||
/>
|
</FormControl>
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{type === "email" && (
|
{type === "email" && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -16,9 +16,11 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
import { generateSHA256Hash } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect } from "react";
|
import { useTranslation } from "next-i18next";
|
||||||
|
import { useEffect, useMemo, 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";
|
||||||
@@ -51,6 +53,15 @@ const randomImages = [
|
|||||||
export const ProfileForm = () => {
|
export const ProfileForm = () => {
|
||||||
const { data, refetch } = api.auth.get.useQuery();
|
const { data, refetch } = api.auth.get.useQuery();
|
||||||
const { mutateAsync, isLoading } = api.auth.update.useMutation();
|
const { mutateAsync, isLoading } = api.auth.update.useMutation();
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
|
const [gravatarHash, setGravatarHash] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const availableAvatars = useMemo(() => {
|
||||||
|
if (gravatarHash === null) return randomImages;
|
||||||
|
return randomImages.concat([
|
||||||
|
`https://www.gravatar.com/avatar/${gravatarHash}`,
|
||||||
|
]);
|
||||||
|
}, [gravatarHash]);
|
||||||
|
|
||||||
const form = useForm<Profile>({
|
const form = useForm<Profile>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -68,6 +79,12 @@ export const ProfileForm = () => {
|
|||||||
password: "",
|
password: "",
|
||||||
image: data?.image || "",
|
image: data?.image || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (data.email) {
|
||||||
|
generateSHA256Hash(data.email).then((hash) => {
|
||||||
|
setGravatarHash(hash);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
form.reset();
|
form.reset();
|
||||||
}, [form, form.reset, data]);
|
}, [form, form.reset, data]);
|
||||||
@@ -91,10 +108,10 @@ export const ProfileForm = () => {
|
|||||||
<Card className="bg-transparent">
|
<Card className="bg-transparent">
|
||||||
<CardHeader className="flex flex-row gap-2 flex-wrap justify-between items-center">
|
<CardHeader className="flex flex-row gap-2 flex-wrap justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-xl">Account</CardTitle>
|
<CardTitle className="text-xl">
|
||||||
<CardDescription>
|
{t("settings.profile.title")}
|
||||||
Change the details of your profile here.
|
</CardTitle>
|
||||||
</CardDescription>
|
<CardDescription>{t("settings.profile.description")}</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
{!data?.is2FAEnabled ? <Enable2FA /> : <Disable2FA />}
|
{!data?.is2FAEnabled ? <Enable2FA /> : <Disable2FA />}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -107,9 +124,12 @@ export const ProfileForm = () => {
|
|||||||
name="email"
|
name="email"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Email</FormLabel>
|
<FormLabel>{t("settings.profile.email")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Email" {...field} />
|
<Input
|
||||||
|
placeholder={t("settings.profile.email")}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -120,11 +140,11 @@ export const ProfileForm = () => {
|
|||||||
name="password"
|
name="password"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Password</FormLabel>
|
<FormLabel>{t("settings.profile.password")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Password"
|
placeholder={t("settings.profile.password")}
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value || ""}
|
value={field.value || ""}
|
||||||
/>
|
/>
|
||||||
@@ -139,7 +159,7 @@ export const ProfileForm = () => {
|
|||||||
name="image"
|
name="image"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Avatar</FormLabel>
|
<FormLabel>{t("settings.profile.avatar")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
onValueChange={(e) => {
|
onValueChange={(e) => {
|
||||||
@@ -149,7 +169,7 @@ export const ProfileForm = () => {
|
|||||||
value={field.value}
|
value={field.value}
|
||||||
className="flex flex-row flex-wrap gap-2 max-xl:justify-center"
|
className="flex flex-row flex-wrap gap-2 max-xl:justify-center"
|
||||||
>
|
>
|
||||||
{randomImages.map((image) => (
|
{availableAvatars.map((image) => (
|
||||||
<FormItem key={image}>
|
<FormItem key={image}>
|
||||||
<FormLabel className="[&:has([data-state=checked])>img]:border-primary [&:has([data-state=checked])>img]:border-1 [&:has([data-state=checked])>img]:p-px cursor-pointer">
|
<FormLabel className="[&:has([data-state=checked])>img]:border-primary [&:has([data-state=checked])>img]:border-1 [&:has([data-state=checked])>img]:p-px cursor-pointer">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -177,7 +197,7 @@ export const ProfileForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button type="submit" isLoading={isLoading}>
|
<Button type="submit" isLoading={isLoading}>
|
||||||
Save
|
{t("settings.common.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { UpdateServerIp } from "@/components/dashboard/settings/web-server/update-server-ip";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -11,10 +12,13 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ShowModalLogs } from "../../web-server/show-modal-logs";
|
import { ShowModalLogs } from "../../web-server/show-modal-logs";
|
||||||
|
import { GPUSupportModal } from "../gpu-support-modal";
|
||||||
|
|
||||||
export const ShowDokployActions = () => {
|
export const ShowDokployActions = () => {
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
const { mutateAsync: reloadServer, isLoading } =
|
const { mutateAsync: reloadServer, isLoading } =
|
||||||
api.settings.reloadServer.useMutation();
|
api.settings.reloadServer.useMutation();
|
||||||
|
|
||||||
@@ -22,11 +26,13 @@ export const ShowDokployActions = () => {
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild disabled={isLoading}>
|
<DropdownMenuTrigger asChild disabled={isLoading}>
|
||||||
<Button isLoading={isLoading} variant="outline">
|
<Button isLoading={isLoading} variant="outline">
|
||||||
Server
|
{t("settings.server.webServer.server.label")}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-56" align="start">
|
<DropdownMenuContent className="w-56" align="start">
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
|
{t("settings.server.webServer.actions")}
|
||||||
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -39,12 +45,27 @@ export const ShowDokployActions = () => {
|
|||||||
toast.success("Server Reloaded");
|
toast.success("Server Reloaded");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
<span>Reload</span>
|
<span>{t("settings.server.webServer.reload")}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<ShowModalLogs appName="dokploy">
|
<ShowModalLogs appName="dokploy">
|
||||||
<span>Watch logs</span>
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{t("settings.server.webServer.watchLogs")}
|
||||||
|
</DropdownMenuItem>
|
||||||
</ShowModalLogs>
|
</ShowModalLogs>
|
||||||
|
<GPUSupportModal />
|
||||||
|
<UpdateServerIp>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{t("settings.server.webServer.updateServerIp")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</UpdateServerIp>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { CardDescription, CardTitle } from "@/components/ui/card";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -21,13 +20,13 @@ export const ShowServerActions = ({ serverId }: Props) => {
|
|||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="w-full cursor-pointer "
|
className="w-full cursor-pointer"
|
||||||
onSelect={(e) => e.preventDefault()}
|
onSelect={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
View Actions
|
View Actions
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-xl overflow-y-auto max-h-screen ">
|
<DialogContent className="sm:max-w-xl overflow-y-auto max-h-screen">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<DialogTitle className="text-xl">Web server settings</DialogTitle>
|
<DialogTitle className="text-xl">Web server settings</DialogTitle>
|
||||||
<DialogDescription>Reload or clean the web server.</DialogDescription>
|
<DialogDescription>Reload or clean the web server.</DialogDescription>
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
}
|
}
|
||||||
export const ShowStorageActions = ({ serverId }: Props) => {
|
export const ShowStorageActions = ({ serverId }: Props) => {
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } =
|
const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } =
|
||||||
api.settings.cleanAll.useMutation();
|
api.settings.cleanAll.useMutation();
|
||||||
|
|
||||||
@@ -64,11 +66,13 @@ export const ShowStorageActions = ({ serverId }: Props) => {
|
|||||||
}
|
}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
Space
|
{t("settings.server.webServer.storage.label")}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-64" align="start">
|
<DropdownMenuContent className="w-64" align="start">
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
|
{t("settings.server.webServer.actions")}
|
||||||
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -85,7 +89,9 @@ export const ShowStorageActions = ({ serverId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>Clean unused images</span>
|
<span>
|
||||||
|
{t("settings.server.webServer.storage.cleanUnusedImages")}
|
||||||
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="w-full cursor-pointer"
|
className="w-full cursor-pointer"
|
||||||
@@ -101,7 +107,9 @@ export const ShowStorageActions = ({ serverId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>Clean unused volumes</span>
|
<span>
|
||||||
|
{t("settings.server.webServer.storage.cleanUnusedVolumes")}
|
||||||
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -118,7 +126,9 @@ export const ShowStorageActions = ({ serverId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>Clean stopped containers</span>
|
<span>
|
||||||
|
{t("settings.server.webServer.storage.cleanStoppedContainers")}
|
||||||
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -135,7 +145,9 @@ export const ShowStorageActions = ({ serverId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>Clean Docker Builder & System</span>
|
<span>
|
||||||
|
{t("settings.server.webServer.storage.cleanDockerBuilder")}
|
||||||
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{!serverId && (
|
{!serverId && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -150,7 +162,9 @@ export const ShowStorageActions = ({ serverId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>Clean Monitoring </span>
|
<span>
|
||||||
|
{t("settings.server.webServer.storage.cleanMonitoring")}
|
||||||
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -168,7 +182,7 @@ export const ShowStorageActions = ({ serverId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>Clean all</span>
|
<span>{t("settings.server.webServer.storage.cleanAll")}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { api } from "@/utils/api";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
|
import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
|
||||||
import { ShowModalLogs } from "../../web-server/show-modal-logs";
|
import { ShowModalLogs } from "../../web-server/show-modal-logs";
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ interface Props {
|
|||||||
serverId?: string;
|
serverId?: string;
|
||||||
}
|
}
|
||||||
export const ShowTraefikActions = ({ serverId }: Props) => {
|
export const ShowTraefikActions = ({ serverId }: Props) => {
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } =
|
const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } =
|
||||||
api.settings.reloadTraefik.useMutation();
|
api.settings.reloadTraefik.useMutation();
|
||||||
|
|
||||||
@@ -51,11 +53,13 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
|
|||||||
isLoading={reloadTraefikIsLoading || toggleDashboardIsLoading}
|
isLoading={reloadTraefikIsLoading || toggleDashboardIsLoading}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
Traefik
|
{t("settings.server.webServer.traefik.label")}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-56" align="start">
|
<DropdownMenuContent className="w-56" align="start">
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
|
{t("settings.server.webServer.actions")}
|
||||||
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -70,18 +74,24 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
|
|||||||
toast.error("Error to reload the traefik");
|
toast.error("Error to reload the traefik");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
<span>Reload</span>
|
<span>{t("settings.server.webServer.reload")}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<ShowModalLogs appName="dokploy-traefik" serverId={serverId}>
|
<ShowModalLogs appName="dokploy-traefik" serverId={serverId}>
|
||||||
<span>Watch logs</span>
|
<DropdownMenuItem
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
{t("settings.server.webServer.watchLogs")}
|
||||||
|
</DropdownMenuItem>
|
||||||
</ShowModalLogs>
|
</ShowModalLogs>
|
||||||
<EditTraefikEnv serverId={serverId}>
|
<EditTraefikEnv serverId={serverId}>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onSelect={(e) => e.preventDefault()}
|
onSelect={(e) => e.preventDefault()}
|
||||||
className="w-full cursor-pointer space-x-3"
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
<span>Modify Env</span>
|
<span>{t("settings.server.webServer.traefik.modifyEnv")}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</EditTraefikEnv>
|
</EditTraefikEnv>
|
||||||
|
|
||||||
|
|||||||
@@ -23,29 +23,27 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
|||||||
const enabled = data?.enableDockerCleanup || server?.enableDockerCleanup;
|
const enabled = data?.enableDockerCleanup || server?.enableDockerCleanup;
|
||||||
|
|
||||||
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();
|
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();
|
||||||
|
|
||||||
|
const handleToggle = async (checked: boolean) => {
|
||||||
|
try {
|
||||||
|
await mutateAsync({
|
||||||
|
enableDockerCleanup: checked,
|
||||||
|
serverId: serverId,
|
||||||
|
});
|
||||||
|
if (serverId) {
|
||||||
|
await refetchServer();
|
||||||
|
} else {
|
||||||
|
await refetch();
|
||||||
|
}
|
||||||
|
toast.success("Docker Cleanup updated");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Docker Cleanup Error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Switch
|
<Switch checked={!!enabled} onCheckedChange={handleToggle} />
|
||||||
checked={enabled}
|
|
||||||
onCheckedChange={async (e) => {
|
|
||||||
await mutateAsync({
|
|
||||||
enableDockerCleanup: e,
|
|
||||||
serverId: serverId,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Docker Cleanup Enabled");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Docker Cleanup Error");
|
|
||||||
});
|
|
||||||
|
|
||||||
if (serverId) {
|
|
||||||
refetchServer();
|
|
||||||
} else {
|
|
||||||
refetch();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Label className="text-primary">Daily Docker Cleanup</Label>
|
<Label className="text-primary">Daily Docker Cleanup</Label>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { GPUSupport } from "./gpu-support";
|
||||||
|
|
||||||
|
export const GPUSupportModal = () => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<span>GPU Setup</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-4xl overflow-y-auto max-h-screen">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
Dokploy Server GPU Setup
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<GPUSupport serverId="" />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { TRPCClientError } from "@trpc/client";
|
||||||
|
import { CheckCircle2, Cpu, Loader2, RefreshCw, XCircle } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface GPUSupportProps {
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GPUSupport({ serverId }: GPUSupportProps) {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
const utils = api.useContext();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: gpuStatus,
|
||||||
|
isLoading: isChecking,
|
||||||
|
refetch,
|
||||||
|
} = api.settings.checkGPUStatus.useQuery(
|
||||||
|
{ serverId },
|
||||||
|
{
|
||||||
|
enabled: serverId !== undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const setupGPU = api.settings.setupGPU.useMutation({
|
||||||
|
onMutate: () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
toast.success("GPU support enabled successfully");
|
||||||
|
setIsLoading(false);
|
||||||
|
await utils.settings.checkGPUStatus.invalidate({ serverId });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(
|
||||||
|
error.message ||
|
||||||
|
"Failed to enable GPU support. Please check server logs.",
|
||||||
|
);
|
||||||
|
setIsLoading(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setIsRefreshing(true);
|
||||||
|
try {
|
||||||
|
await utils.settings.checkGPUStatus.invalidate({ serverId });
|
||||||
|
await refetch();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Failed to refresh GPU status");
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
handleRefresh();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleEnableGPU = async () => {
|
||||||
|
if (serverId === undefined) {
|
||||||
|
toast.error("No server selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await setupGPU.mutateAsync({ serverId });
|
||||||
|
} catch (error) {
|
||||||
|
// Error handling is done in mutation's onError
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
||||||
|
<div className="flex flex-row gap-2 justify-between w-full items-end max-sm:flex-col">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Cpu className="size-5" />
|
||||||
|
<CardTitle className="text-xl">GPU Configuration</CardTitle>
|
||||||
|
</div>
|
||||||
|
<CardDescription>
|
||||||
|
Configure and monitor GPU support
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<DialogAction
|
||||||
|
title="Enable GPU Support?"
|
||||||
|
description="This will enable GPU support for Docker Swarm on this server. Make sure you have the required hardware and drivers installed."
|
||||||
|
onClick={handleEnableGPU}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
disabled={isLoading || serverId === undefined || isChecking}
|
||||||
|
>
|
||||||
|
{isLoading
|
||||||
|
? "Enabling GPU..."
|
||||||
|
: gpuStatus?.swarmEnabled
|
||||||
|
? "Reconfigure GPU"
|
||||||
|
: "Enable GPU"}
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
disabled={isChecking || isRefreshing}
|
||||||
|
>
|
||||||
|
<RefreshCw
|
||||||
|
className={`h-5 w-5 ${isChecking || isRefreshing ? "animate-spin" : ""}`}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
<AlertBlock type="info">
|
||||||
|
<div className="font-medium mb-2">System Requirements:</div>
|
||||||
|
<ul className="list-disc list-inside text-sm space-y-1">
|
||||||
|
<li>NVIDIA GPU hardware must be physically installed</li>
|
||||||
|
<li>
|
||||||
|
NVIDIA drivers must be installed and running (check with
|
||||||
|
nvidia-smi)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
NVIDIA Container Runtime must be installed
|
||||||
|
(nvidia-container-runtime)
|
||||||
|
</li>
|
||||||
|
<li>User must have sudo/administrative privileges</li>
|
||||||
|
<li>System must support CUDA for GPU acceleration</li>
|
||||||
|
</ul>
|
||||||
|
</AlertBlock>
|
||||||
|
|
||||||
|
{isChecking ? (
|
||||||
|
<div className="flex items-center justify-center text-muted-foreground py-4">
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
<span>Checking GPU status...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{/* Prerequisites Section */}
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
|
<h3 className="text-lg font-semibold mb-1">Prerequisites</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Shows all software checks and available hardware
|
||||||
|
</p>
|
||||||
|
<div className="grid gap-2.5">
|
||||||
|
<StatusRow
|
||||||
|
label="NVIDIA Driver"
|
||||||
|
isEnabled={gpuStatus?.driverInstalled}
|
||||||
|
description={
|
||||||
|
gpuStatus?.driverVersion
|
||||||
|
? `Installed (v${gpuStatus.driverVersion})`
|
||||||
|
: "Not Installed"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="GPU Model"
|
||||||
|
value={gpuStatus?.gpuModel || "Not Detected"}
|
||||||
|
showIcon={false}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="GPU Memory"
|
||||||
|
value={gpuStatus?.memoryInfo || "Not Available"}
|
||||||
|
showIcon={false}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="Available GPUs"
|
||||||
|
value={gpuStatus?.availableGPUs || 0}
|
||||||
|
showIcon={false}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="CUDA Support"
|
||||||
|
isEnabled={gpuStatus?.cudaSupport}
|
||||||
|
description={
|
||||||
|
gpuStatus?.cudaVersion
|
||||||
|
? `Available (v${gpuStatus.cudaVersion})`
|
||||||
|
: "Not Available"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="NVIDIA Container Runtime"
|
||||||
|
isEnabled={gpuStatus?.runtimeInstalled}
|
||||||
|
description={
|
||||||
|
gpuStatus?.runtimeInstalled
|
||||||
|
? "Installed"
|
||||||
|
: "Not Installed"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Configuration Status */}
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
|
<h3 className="text-lg font-semibold mb-1">
|
||||||
|
Docker Swarm GPU Status
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Shows the configuration state that changes with the Enable
|
||||||
|
GPU
|
||||||
|
</p>
|
||||||
|
<div className="grid gap-2.5">
|
||||||
|
<StatusRow
|
||||||
|
label="Runtime Configuration"
|
||||||
|
isEnabled={gpuStatus?.runtimeConfigured}
|
||||||
|
description={
|
||||||
|
gpuStatus?.runtimeConfigured
|
||||||
|
? "Default Runtime"
|
||||||
|
: "Not Default Runtime"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="Swarm GPU Support"
|
||||||
|
isEnabled={gpuStatus?.swarmEnabled}
|
||||||
|
description={
|
||||||
|
gpuStatus?.swarmEnabled
|
||||||
|
? `Enabled (${gpuStatus.gpuResources} GPU${gpuStatus.gpuResources !== 1 ? "s" : ""})`
|
||||||
|
: "Not Enabled"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatusRowProps {
|
||||||
|
label: string;
|
||||||
|
isEnabled?: boolean;
|
||||||
|
description?: string;
|
||||||
|
value?: string | number;
|
||||||
|
showIcon?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatusRow({
|
||||||
|
label,
|
||||||
|
isEnabled,
|
||||||
|
description,
|
||||||
|
value,
|
||||||
|
showIcon = true,
|
||||||
|
}: StatusRowProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm">{label}</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{showIcon ? (
|
||||||
|
<>
|
||||||
|
{isEnabled ? (
|
||||||
|
<CheckCircle2 className="size-4 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="size-4 text-red-500" />
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={`text-sm ${isEnabled ? "text-green-500" : "text-red-500"}`}
|
||||||
|
>
|
||||||
|
{description || (isEnabled ? "Installed" : "Not Installed")}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-muted-foreground">{value}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ import Link from "next/link";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ShowDeployment } from "../../application/deployments/show-deployment";
|
import { ShowDeployment } from "../../application/deployments/show-deployment";
|
||||||
|
import { GPUSupport } from "./gpu-support";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
@@ -89,9 +90,10 @@ export const SetupServer = ({ serverId }: Props) => {
|
|||||||
) : (
|
) : (
|
||||||
<div id="hook-form-add-gitlab" className="grid w-full gap-1">
|
<div id="hook-form-add-gitlab" className="grid w-full gap-1">
|
||||||
<Tabs defaultValue="ssh-keys">
|
<Tabs defaultValue="ssh-keys">
|
||||||
<TabsList className="grid grid-cols-2 w-[400px]">
|
<TabsList className="grid grid-cols-3 w-[400px]">
|
||||||
<TabsTrigger value="ssh-keys">SSH Keys</TabsTrigger>
|
<TabsTrigger value="ssh-keys">SSH Keys</TabsTrigger>
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
|
<TabsTrigger value="gpu-setup">GPU Setup</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent
|
<TabsContent
|
||||||
value="ssh-keys"
|
value="ssh-keys"
|
||||||
@@ -291,6 +293,14 @@ export const SetupServer = ({ serverId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent
|
||||||
|
value="gpu-setup"
|
||||||
|
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2 text-sm text-muted-foreground pt-3">
|
||||||
|
<GPUSupport serverId={serverId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export const ShowServers = () => {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
{data && data?.length > 0 && (
|
{data && data?.length > 0 && (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6 overflow-auto">
|
||||||
<Table>
|
<Table>
|
||||||
<TableCaption>See all servers</TableCaption>
|
<TableCaption>See all servers</TableCaption>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
@@ -228,21 +228,17 @@ export const ShowServers = () => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
|
|
||||||
{isActive && (
|
{isActive && server.sshKeyId && (
|
||||||
<>
|
<>
|
||||||
{server.sshKeyId && (
|
<DropdownMenuSeparator />
|
||||||
<>
|
<DropdownMenuLabel>Extra</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuLabel>Extra</DropdownMenuLabel>
|
|
||||||
|
|
||||||
<ShowTraefikFileSystemModal
|
<ShowTraefikFileSystemModal
|
||||||
serverId={server.serverId}
|
serverId={server.serverId}
|
||||||
/>
|
/>
|
||||||
<ShowDockerContainersModal
|
<ShowDockerContainersModal
|
||||||
serverId={server.serverId}
|
serverId={server.serverId}
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} 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 { useTranslation } from "next-i18next";
|
||||||
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";
|
||||||
@@ -49,6 +50,7 @@ const addServerDomain = z
|
|||||||
type AddServerDomain = z.infer<typeof addServerDomain>;
|
type AddServerDomain = z.infer<typeof addServerDomain>;
|
||||||
|
|
||||||
export const WebDomain = () => {
|
export const WebDomain = () => {
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
const { data: user, refetch } = api.admin.one.useQuery();
|
const { data: user, refetch } = api.admin.one.useQuery();
|
||||||
const { mutateAsync, isLoading } =
|
const { mutateAsync, isLoading } =
|
||||||
api.settings.assignDomainServer.useMutation();
|
api.settings.assignDomainServer.useMutation();
|
||||||
@@ -89,9 +91,11 @@ export const WebDomain = () => {
|
|||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Card className="bg-transparent">
|
<Card className="bg-transparent">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Server Domain</CardTitle>
|
<CardTitle className="text-xl">
|
||||||
|
{t("settings.server.domain.title")}
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Add a domain to your server application.
|
{t("settings.server.domain.description")}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
<CardContent className="flex w-full flex-col gap-4">
|
||||||
@@ -106,7 +110,9 @@ export const WebDomain = () => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Domain</FormLabel>
|
<FormLabel>
|
||||||
|
{t("settings.server.domain.form.domain")}
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@@ -126,7 +132,9 @@ export const WebDomain = () => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Letsencrypt Email</FormLabel>
|
<FormLabel>
|
||||||
|
{t("settings.server.domain.form.letsEncryptEmail")}
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@@ -145,20 +153,32 @@ export const WebDomain = () => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem className="md:col-span-2">
|
<FormItem className="md:col-span-2">
|
||||||
<FormLabel>Certificate</FormLabel>
|
<FormLabel>
|
||||||
|
{t("settings.server.domain.form.certificate.label")}
|
||||||
|
</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a certificate" />
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
"settings.server.domain.form.certificate.placeholder",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value={"none"}>None</SelectItem>
|
<SelectItem value={"none"}>
|
||||||
|
{t(
|
||||||
|
"settings.server.domain.form.certificateOptions.none",
|
||||||
|
)}
|
||||||
|
</SelectItem>
|
||||||
<SelectItem value={"letsencrypt"}>
|
<SelectItem value={"letsencrypt"}>
|
||||||
Letsencrypt (Default)
|
{t(
|
||||||
|
"settings.server.domain.form.certificateOptions.letsencrypt",
|
||||||
|
)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -169,7 +189,7 @@ export const WebDomain = () => {
|
|||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Button isLoading={isLoading} type="submit">
|
<Button isLoading={isLoading} type="submit">
|
||||||
Save
|
{t("settings.common.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ShowDokployActions } from "./servers/actions/show-dokploy-actions";
|
import { ShowDokployActions } from "./servers/actions/show-dokploy-actions";
|
||||||
import { ShowStorageActions } from "./servers/actions/show-storage-actions";
|
import { ShowStorageActions } from "./servers/actions/show-storage-actions";
|
||||||
@@ -18,6 +19,7 @@ interface Props {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
export const WebServer = ({ className }: Props) => {
|
export const WebServer = ({ className }: Props) => {
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
const { data } = api.admin.one.useQuery();
|
const { data } = api.admin.one.useQuery();
|
||||||
|
|
||||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||||
@@ -25,8 +27,12 @@ export const WebServer = ({ className }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Card className={cn("rounded-lg w-full bg-transparent p-0", className)}>
|
<Card className={cn("rounded-lg w-full bg-transparent p-0", className)}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Web server settings</CardTitle>
|
<CardTitle className="text-xl">
|
||||||
<CardDescription>Reload or clean the web server.</CardDescription>
|
{t("settings.server.webServer.title")}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{t("settings.server.webServer.description")}
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4 ">
|
<CardContent className="flex flex-col gap-4 ">
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
|||||||
@@ -58,14 +58,7 @@ export const ShowModalLogs = ({ appName, children, serverId }: Props) => {
|
|||||||
}, [data]);
|
}, [data]);
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer space-x-3"
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-7xl">
|
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-7xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>View Logs</DialogTitle>
|
<DialogTitle>View Logs</DialogTitle>
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { RefreshCw } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
serverIp: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Schema = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpdateServerIp = ({ children, serverId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const { data } = api.admin.one.useQuery();
|
||||||
|
const { data: ip } = api.server.publicIp.useQuery();
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
|
api.admin.update.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<Schema>({
|
||||||
|
defaultValues: {
|
||||||
|
serverIp: data?.serverIp || "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
serverIp: data.serverIp || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form, form.reset, data]);
|
||||||
|
|
||||||
|
const utils = api.useUtils();
|
||||||
|
|
||||||
|
const setCurrentIp = () => {
|
||||||
|
if (!ip) return;
|
||||||
|
form.setValue("serverIp", ip);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data: Schema) => {
|
||||||
|
await mutateAsync({
|
||||||
|
serverIp: data.serverIp,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Server IP Updated");
|
||||||
|
await utils.admin.one.invalidate();
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to update the IP of the server");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Update Server IP</DialogTitle>
|
||||||
|
<DialogDescription>Update the IP of the server</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form-update-server-ip"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="serverIp"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Server IP</FormLabel>
|
||||||
|
<FormControl className="flex gap-2">
|
||||||
|
<div>
|
||||||
|
<Input {...field} />
|
||||||
|
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={setCurrentIp}
|
||||||
|
>
|
||||||
|
<RefreshCw className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[11rem]"
|
||||||
|
>
|
||||||
|
<p>Set current public IP</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<pre>
|
||||||
|
<FormMessage />
|
||||||
|
</pre>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
disabled={isLoading}
|
||||||
|
form="hook-form-update-server-ip"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -48,8 +48,8 @@ export const UpdateServer = () => {
|
|||||||
<li>Some bug that is blocking to use some features</li>
|
<li>Some bug that is blocking to use some features</li>
|
||||||
</ul>
|
</ul>
|
||||||
<AlertBlock type="info">
|
<AlertBlock type="info">
|
||||||
Please we recommend to see the latest version to see if there are
|
We recommend checking the latest version for any breaking changes
|
||||||
any breaking changes before updating. Go to{" "}
|
before updating. Go to{" "}
|
||||||
<Link
|
<Link
|
||||||
href="https://github.com/Dokploy/dokploy/releases"
|
href="https://github.com/Dokploy/dokploy/releases"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -87,7 +87,7 @@ export const UpdateServer = () => {
|
|||||||
}}
|
}}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
>
|
>
|
||||||
Check updates
|
Check Updates
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -161,29 +161,27 @@ export const GitlabIcon = ({ className }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
aria-label="gitlab"
|
aria-label="gitlab"
|
||||||
height="14"
|
|
||||||
viewBox="0 0 24 22"
|
|
||||||
width="14"
|
width="14"
|
||||||
className={cn("fill-white text-white", className)}
|
height="14"
|
||||||
|
viewBox="0 0 14 14"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className={cn("text-white", className)}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M1.279 8.29L.044 12.294c-.117.367 0 .78.325 1.014l11.323 8.23-.009-.012-.03-.039L1.279 8.29zM22.992 13.308a.905.905 0 00.325-1.014L22.085 8.29 11.693 21.52l11.299-8.212z"
|
d="m13.767 5.854-.02-.05L11.842.83a.5.5 0 0 0-.493-.312.5.5 0 0 0-.287.107.5.5 0 0 0-.169.257L9.607 4.819h-5.21L3.11.883A.5.5 0 0 0 2.162.83L.252 5.801l-.018.05a3.54 3.54 0 0 0 1.173 4.09l.007.005.017.012 2.903 2.174L5.77 13.22l.875.66a.59.59 0 0 0 .711 0l.875-.66 1.436-1.087 2.92-2.187.008-.006a3.54 3.54 0 0 0 1.172-4.085"
|
||||||
fill="currentColor"
|
fill="#E24329"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M1.279 8.29l10.374 13.197.03.039.01-.006L22.085 8.29H1.28z"
|
d="m13.767 5.854-.02-.05a6.4 6.4 0 0 0-2.562 1.152L7 10.12l2.666 2.015 2.92-2.187.007-.006a3.54 3.54 0 0 0 1.174-4.088"
|
||||||
fill="currentColor"
|
fill="#FC6D26"
|
||||||
opacity="0.4"
|
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M15.982 8.29l-4.299 13.236-.004.011.014-.017L22.085 8.29h-6.103zM7.376 8.29H1.279l10.374 13.197L7.376 8.29z"
|
d="m4.334 12.135 1.436 1.087.875.66a.59.59 0 0 0 .711 0l.875-.66 1.436-1.087S8.425 11.195 7 10.12c-1.425 1.075-2.666 2.015-2.666 2.015"
|
||||||
fill="currentColor"
|
fill="#FCA326"
|
||||||
opacity="0.6"
|
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M18.582.308l-2.6 7.982h6.103L19.48.308c-.133-.41-.764-.41-.897 0zM1.279 8.29L3.88.308c.133-.41.764-.41.897 0l2.6 7.982H1.279z"
|
d="M2.814 6.956A6.4 6.4 0 0 0 .253 5.8l-.02.05a3.54 3.54 0 0 0 1.174 4.09l.007.005.017.012 2.903 2.174L7 10.117z"
|
||||||
fill="currentColor"
|
fill="#FC6D26"
|
||||||
opacity="0.4"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
@@ -200,7 +198,7 @@ export const GithubIcon = ({ className }: Props) => {
|
|||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M7 .175c-3.872 0-7 3.128-7 7 0 3.084 2.013 5.71 4.79 6.65.35.066.482-.153.482-.328v-1.181c-1.947.415-2.363-.941-2.363-.941-.328-.81-.787-1.028-.787-1.028-.634-.438.044-.416.044-.416.7.044 1.071.722 1.071.722.635 1.072 1.641.766 2.035.59.066-.459.24-.765.437-.94-1.553-.175-3.193-.787-3.193-3.456 0-.766.262-1.378.721-1.881-.065-.175-.306-.897.066-1.86 0 0 .59-.197 1.925.722a6.754 6.754 0 0 1 1.75-.24c.59 0 1.203.087 1.75.24 1.335-.897 1.925-.722 1.925-.722.372.963.131 1.685.066 1.86.46.48.722 1.115.722 1.88 0 2.691-1.641 3.282-3.194 3.457.24.219.481.634.481 1.29v1.926c0 .197.131.415.481.328C11.988 12.884 14 10.259 14 7.175c0-3.872-3.128-7-7-7z"
|
d="M7 .175c-3.872 0-7 3.128-7 7 0 3.084 2.013 5.71 4.79 6.65.35.066.482-.153.482-.328v-1.181c-1.947.415-2.363-.941-2.363-.941-.328-.81-.787-1.028-.787-1.028-.634-.438.044-.416.044-.416.7.044 1.071.722 1.071.722.635 1.072 1.641.766 2.035.59.066-.459.24-.765.437-.94-1.553-.175-3.193-.787-3.193-3.456 0-.766.262-1.378.721-1.881-.065-.175-.306-.897.066-1.86 0 0 .59-.197 1.925.722a6.754 6.754 0 0 1 1.75-.24c.59 0 1.203.087 1.75.24 1.335-.897 1.925-.722 1.925-.722.372.963.131 1.685.066 1.86.46.48.722 1.115.722 1.88 0 2.691-1.641 3.282-3.194 3.457.24.219.481.634.481 1.29v1.926c0 .197.131.415.481.328C11.988 12.884 14 10.259 14 7.175c0-3.872-3.128-7-7-7z"
|
||||||
fill="#fff"
|
fill="currentColor"
|
||||||
fillRule="nonzero"
|
fillRule="nonzero"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -209,30 +207,34 @@ export const GithubIcon = ({ className }: Props) => {
|
|||||||
|
|
||||||
export const BitbucketIcon = ({ className }: Props) => {
|
export const BitbucketIcon = ({ className }: Props) => {
|
||||||
return (
|
return (
|
||||||
<svg height="14" viewBox="-2 -2 65 59" width="14" className={className}>
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="13"
|
||||||
|
viewBox="0 0 14 13"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M.454 0a.448.448 0 0 0-.448.52l1.903 11.556a.61.61 0 0 0 .597.509h9.132a.45.45 0 0 0 .448-.377L13.994.525a.448.448 0 0 0-.448-.52zM8.47 8.352H5.555l-.79-4.121h4.411z"
|
||||||
|
fill="#2684FF"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M13.384 4.23H9.176L8.47 8.353H5.555L2.113 12.44c.11.095.248.147.393.148h9.134a.45.45 0 0 0 .448-.377z"
|
||||||
|
fill="url(#a)"
|
||||||
|
/>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="bitbucket-:R7aq37rqjt7rrrmpjtuj7l9qjtsr:"
|
id="a"
|
||||||
x1="104.953%"
|
x1="14.357"
|
||||||
x2="46.569%"
|
y1="5.383"
|
||||||
y1="21.921%"
|
x2="7.402"
|
||||||
y2="75.234%"
|
y2="10.814"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
>
|
>
|
||||||
<stop offset="7%" stopColor="currentColor" stopOpacity=".4" />
|
<stop offset=".18" stopColor="#0052CC" />
|
||||||
<stop offset="100%" stopColor="currentColor" />
|
<stop offset="1" stopColor="#2684FF" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<path
|
|
||||||
d="M59.696 18.86h-18.77l-3.15 18.39h-13L9.426 55.47a2.71 2.71 0 001.75.66h40.74a2 2 0 002-1.68l5.78-35.59z"
|
|
||||||
fill="url(#bitbucket-:R7aq37rqjt7rrrmpjtuj7l9qjtsr:)"
|
|
||||||
fillRule="nonzero"
|
|
||||||
transform="translate(-.026 .82)"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M2 .82a2 2 0 00-2 2.32l8.49 51.54a2.7 2.7 0 00.91 1.61 2.71 2.71 0 001.75.66l15.76-18.88H24.7l-3.47-18.39h38.44l2.7-16.53a2 2 0 00-2-2.32L2 .82z"
|
|
||||||
fill="currentColor"
|
|
||||||
fillRule="nonzero"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const StatusTooltip = ({ status, className }: Props) => {
|
|||||||
)}
|
)}
|
||||||
{status === "done" && (
|
{status === "done" && (
|
||||||
<div
|
<div
|
||||||
className={cn("size-3.5 rounded-full bg-primary", className)}
|
className={cn("size-3.5 rounded-full bg-green-500", className)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{status === "running" && (
|
{status === "running" && (
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const buttonVariants = cva(
|
|||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
"bg-destructive text-destructive-foreground hover:bg-destructive/70",
|
||||||
outline:
|
outline:
|
||||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
secondary:
|
secondary:
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ const InputOTPSeparator = React.forwardRef<
|
|||||||
React.ElementRef<"div">,
|
React.ElementRef<"div">,
|
||||||
React.ComponentPropsWithoutRef<"div">
|
React.ComponentPropsWithoutRef<"div">
|
||||||
>(({ ...props }, ref) => (
|
>(({ ...props }, ref) => (
|
||||||
<div ref={ref} role="separator" {...props}>
|
<div ref={ref} {...props}>
|
||||||
<Dot />
|
<Dot />
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
|
|||||||
2
apps/dokploy/drizzle/0043_closed_naoko.sql
Normal file
2
apps/dokploy/drizzle/0043_closed_naoko.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "admin" ADD COLUMN "env" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "project" ADD COLUMN "env" text DEFAULT '' NOT NULL;
|
||||||
1
apps/dokploy/drizzle/0044_sour_true_believers.sql
Normal file
1
apps/dokploy/drizzle/0044_sour_true_believers.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "admin" DROP COLUMN IF EXISTS "env";
|
||||||
1
apps/dokploy/drizzle/0045_smiling_blur.sql
Normal file
1
apps/dokploy/drizzle/0045_smiling_blur.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "destination" ADD COLUMN "provider" text;
|
||||||
1
apps/dokploy/drizzle/0046_purple_sleeper.sql
Normal file
1
apps/dokploy/drizzle/0046_purple_sleeper.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "application" ADD COLUMN "registryUrl" text;
|
||||||
3982
apps/dokploy/drizzle/meta/0043_snapshot.json
Normal file
3982
apps/dokploy/drizzle/meta/0043_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3975
apps/dokploy/drizzle/meta/0044_snapshot.json
Normal file
3975
apps/dokploy/drizzle/meta/0044_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3981
apps/dokploy/drizzle/meta/0045_snapshot.json
Normal file
3981
apps/dokploy/drizzle/meta/0045_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3987
apps/dokploy/drizzle/meta/0046_snapshot.json
Normal file
3987
apps/dokploy/drizzle/meta/0046_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -302,6 +302,34 @@
|
|||||||
"when": 1729984439862,
|
"when": 1729984439862,
|
||||||
"tag": "0042_fancy_havok",
|
"tag": "0042_fancy_havok",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 43,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1731873965888,
|
||||||
|
"tag": "0043_closed_naoko",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 44,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1731875539532,
|
||||||
|
"tag": "0044_sour_true_believers",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 45,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1732644181718,
|
||||||
|
"tag": "0045_smiling_blur",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 46,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1732851191048,
|
||||||
|
"tag": "0046_purple_sleeper",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -4,3 +4,11 @@ import { twMerge } from "tailwind-merge";
|
|||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function generateSHA256Hash(text: string) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const data = encoder.encode(text);
|
||||||
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||||
|
}
|
||||||
|
|||||||
10
apps/dokploy/next-i18next.config.cjs
Normal file
10
apps/dokploy/next-i18next.config.cjs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/** @type {import('next-i18next').UserConfig} */
|
||||||
|
module.exports = {
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: "en",
|
||||||
|
locales: ["en", "pl", "ru", "fr", "de", "tr", "zh-Hant", "zh-Hans", "fa"],
|
||||||
|
localeDetection: false,
|
||||||
|
},
|
||||||
|
fallbackLng: "en",
|
||||||
|
keySeparator: false,
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.10.6",
|
"version": "v0.13.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -84,13 +84,16 @@
|
|||||||
"dotenv": "16.4.5",
|
"dotenv": "16.4.5",
|
||||||
"drizzle-orm": "^0.30.8",
|
"drizzle-orm": "^0.30.8",
|
||||||
"drizzle-zod": "0.5.1",
|
"drizzle-zod": "0.5.1",
|
||||||
|
"i18next": "^23.16.4",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"lucia": "^3.0.1",
|
"lucia": "^3.0.1",
|
||||||
"lucide-react": "^0.312.0",
|
"lucide-react": "^0.312.0",
|
||||||
"nanoid": "3",
|
"nanoid": "3",
|
||||||
"next": "^15.0.1",
|
"next": "^15.0.1",
|
||||||
|
"next-i18next": "^15.3.1",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"node-pty": "1.0.0",
|
"node-pty": "1.0.0",
|
||||||
"node-schedule": "2.1.1",
|
"node-schedule": "2.1.1",
|
||||||
@@ -100,6 +103,7 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "^7.49.3",
|
"react-hook-form": "^7.49.3",
|
||||||
|
"react-i18next": "^15.1.0",
|
||||||
"recharts": "^2.12.7",
|
"recharts": "^2.12.7",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"sonner": "^1.4.0",
|
"sonner": "^1.4.0",
|
||||||
@@ -119,6 +123,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/adm-zip": "^0.5.5",
|
"@types/adm-zip": "^0.5.5",
|
||||||
"@types/bcrypt": "5.0.2",
|
"@types/bcrypt": "5.0.2",
|
||||||
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/lodash": "4.17.4",
|
"@types/lodash": "4.17.4",
|
||||||
"@types/node": "^18.17.0",
|
"@types/node": "^18.17.0",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import "@/styles/globals.css";
|
|||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
|
import { appWithTranslation } from "next-i18next";
|
||||||
import { ThemeProvider } from "next-themes";
|
import { ThemeProvider } from "next-themes";
|
||||||
import type { AppProps } from "next/app";
|
import type { AppProps } from "next/app";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
@@ -27,13 +28,14 @@ const MyApp = ({
|
|||||||
pageProps: { ...pageProps },
|
pageProps: { ...pageProps },
|
||||||
}: AppPropsWithLayout) => {
|
}: AppPropsWithLayout) => {
|
||||||
const getLayout = Component.getLayout ?? ((page) => page);
|
const getLayout = Component.getLayout ?? ((page) => page);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<style jsx global>{`
|
<style jsx global>{`
|
||||||
:root {
|
:root {
|
||||||
--font-inter: ${inter.style.fontFamily};
|
--font-inter: ${inter.style.fontFamily};
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Dokploy</title>
|
<title>Dokploy</title>
|
||||||
</Head>
|
</Head>
|
||||||
@@ -59,4 +61,31 @@ const MyApp = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default api.withTRPC(MyApp);
|
export default api.withTRPC(
|
||||||
|
appWithTranslation(
|
||||||
|
MyApp,
|
||||||
|
// keep this in sync with next-i18next.config.js
|
||||||
|
// if you want to know why don't just import the config file, this because next-i18next.config.js must be a CJS, but the rest of the code is ESM.
|
||||||
|
// Add the config here is due to the issue: https://github.com/i18next/next-i18next/issues/2259
|
||||||
|
// if one day every page is translated, we can safely remove this config.
|
||||||
|
{
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: "en",
|
||||||
|
locales: [
|
||||||
|
"en",
|
||||||
|
"pl",
|
||||||
|
"ru",
|
||||||
|
"fr",
|
||||||
|
"de",
|
||||||
|
"tr",
|
||||||
|
"zh-Hant",
|
||||||
|
"zh-Hans",
|
||||||
|
"fa",
|
||||||
|
],
|
||||||
|
localeDetection: false,
|
||||||
|
},
|
||||||
|
fallbackLng: "en",
|
||||||
|
keySeparator: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|||||||
@@ -10,137 +10,138 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||||||
import { extractCommitMessage, extractHash } from "./[refreshToken]";
|
import { extractCommitMessage, extractHash } from "./[refreshToken]";
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse,
|
res: NextApiResponse
|
||||||
) {
|
) {
|
||||||
const signature = req.headers["x-hub-signature-256"];
|
const signature = req.headers["x-hub-signature-256"];
|
||||||
const githubBody = req.body;
|
const githubBody = req.body;
|
||||||
|
|
||||||
if (!githubBody?.installation?.id) {
|
if (!githubBody?.installation?.id) {
|
||||||
res.status(400).json({ message: "Github Installation not found" });
|
res.status(400).json({ message: "Github Installation not found" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const githubResult = await db.query.github.findFirst({
|
const githubResult = await db.query.github.findFirst({
|
||||||
where: eq(github.githubInstallationId, githubBody.installation.id),
|
where: eq(github.githubInstallationId, githubBody.installation.id),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!githubResult) {
|
if (!githubResult) {
|
||||||
res.status(400).json({ message: "Github Installation not found" });
|
res.status(400).json({ message: "Github Installation not found" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!githubResult.githubWebhookSecret) {
|
if (!githubResult.githubWebhookSecret) {
|
||||||
res.status(400).json({ message: "Github Webhook Secret not set" });
|
res.status(400).json({ message: "Github Webhook Secret not set" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const webhooks = new Webhooks({
|
const webhooks = new Webhooks({
|
||||||
secret: githubResult.githubWebhookSecret,
|
secret: githubResult.githubWebhookSecret,
|
||||||
});
|
});
|
||||||
|
|
||||||
const verified = await webhooks.verify(
|
const verified = await webhooks.verify(
|
||||||
JSON.stringify(githubBody),
|
JSON.stringify(githubBody),
|
||||||
signature as string,
|
signature as string
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!verified) {
|
if (!verified) {
|
||||||
res.status(401).json({ message: "Unauthorized" });
|
res.status(401).json({ message: "Unauthorized" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.headers["x-github-event"] === "ping") {
|
if (req.headers["x-github-event"] === "ping") {
|
||||||
res.status(200).json({ message: "Ping received, webhook is active" });
|
res.status(200).json({ message: "Ping received, webhook is active" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.headers["x-github-event"] !== "push") {
|
if (req.headers["x-github-event"] !== "push") {
|
||||||
res.status(400).json({ message: "We only accept push events" });
|
res.status(400).json({ message: "We only accept push events" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const branchName = githubBody?.ref?.replace("refs/heads/", "");
|
const branchName = githubBody?.ref?.replace("refs/heads/", "");
|
||||||
const repository = githubBody?.repository?.name;
|
const repository = githubBody?.repository?.name;
|
||||||
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
||||||
const deploymentHash = extractHash(req.headers, req.body);
|
const deploymentHash = extractHash(req.headers, req.body);
|
||||||
|
|
||||||
const apps = await db.query.applications.findMany({
|
const apps = await db.query.applications.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
eq(applications.sourceType, "github"),
|
eq(applications.sourceType, "github"),
|
||||||
eq(applications.autoDeploy, true),
|
eq(applications.autoDeploy, true),
|
||||||
eq(applications.branch, branchName),
|
eq(applications.branch, branchName),
|
||||||
eq(applications.repository, repository),
|
eq(applications.repository, repository)
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const app of apps) {
|
for (const app of apps) {
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
applicationId: app.applicationId as string,
|
applicationId: app.applicationId as string,
|
||||||
titleLog: deploymentTitle,
|
titleLog: deploymentTitle,
|
||||||
descriptionLog: `Hash: ${deploymentHash}`,
|
descriptionLog: `Hash: ${deploymentHash}`,
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "application",
|
applicationType: "application",
|
||||||
server: !!app.serverId,
|
server: !!app.serverId,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (IS_CLOUD && app.serverId) {
|
if (IS_CLOUD && app.serverId) {
|
||||||
jobData.serverId = app.serverId;
|
jobData.serverId = app.serverId;
|
||||||
await deploy(jobData);
|
await deploy(jobData);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
{ ...jobData },
|
{ ...jobData },
|
||||||
{
|
{
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const composeApps = await db.query.compose.findMany({
|
const composeApps = await db.query.compose.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
eq(compose.sourceType, "github"),
|
eq(compose.sourceType, "github"),
|
||||||
eq(compose.autoDeploy, true),
|
eq(compose.autoDeploy, true),
|
||||||
eq(compose.branch, branchName),
|
eq(compose.branch, branchName),
|
||||||
eq(compose.repository, repository),
|
eq(compose.repository, repository)
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const composeApp of composeApps) {
|
for (const composeApp of composeApps) {
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
composeId: composeApp.composeId as string,
|
composeId: composeApp.composeId as string,
|
||||||
titleLog: deploymentTitle,
|
titleLog: deploymentTitle,
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "compose",
|
applicationType: "compose",
|
||||||
descriptionLog: `Hash: ${deploymentHash}`,
|
descriptionLog: `Hash: ${deploymentHash}`,
|
||||||
};
|
server: !!composeApp.serverId,
|
||||||
|
};
|
||||||
|
|
||||||
if (IS_CLOUD && composeApp.serverId) {
|
if (IS_CLOUD && composeApp.serverId) {
|
||||||
jobData.serverId = composeApp.serverId;
|
jobData.serverId = composeApp.serverId;
|
||||||
await deploy(jobData);
|
await deploy(jobData);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
{ ...jobData },
|
{ ...jobData },
|
||||||
{
|
{
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalApps = apps.length + composeApps.length;
|
const totalApps = apps.length + composeApps.length;
|
||||||
const emptyApps = totalApps === 0;
|
const emptyApps = totalApps === 0;
|
||||||
|
|
||||||
if (emptyApps) {
|
if (emptyApps) {
|
||||||
res.status(200).json({ message: "No apps to deploy" });
|
res.status(200).json({ message: "No apps to deploy" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(200).json({ message: `Deployed ${totalApps} apps` });
|
res.status(200).json({ message: `Deployed ${totalApps} apps` });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).json({ message: "Error To Deploy Application", error });
|
res.status(400).json({ message: "Error To Deploy Application", error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ export default async function handler(
|
|||||||
.update(admins)
|
.update(admins)
|
||||||
.set({
|
.set({
|
||||||
stripeSubscriptionId: newSubscription.id,
|
stripeSubscriptionId: newSubscription.id,
|
||||||
serversQuantity: 0,
|
|
||||||
stripeCustomerId: newSubscription.customer as string,
|
stripeCustomerId: newSubscription.customer as string,
|
||||||
})
|
})
|
||||||
.where(eq(admins.stripeCustomerId, newSubscription.customer as string))
|
.where(eq(admins.stripeCustomerId, newSubscription.customer as string))
|
||||||
@@ -121,12 +120,6 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
case "customer.subscription.updated": {
|
case "customer.subscription.updated": {
|
||||||
const newSubscription = event.data.object as Stripe.Subscription;
|
const newSubscription = event.data.object as Stripe.Subscription;
|
||||||
await db
|
|
||||||
.update(admins)
|
|
||||||
.set({
|
|
||||||
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
|
|
||||||
})
|
|
||||||
.where(eq(admins.stripeCustomerId, newSubscription.customer as string));
|
|
||||||
|
|
||||||
const admin = await findAdminByStripeCustomerId(
|
const admin = await findAdminByStripeCustomerId(
|
||||||
newSubscription.customer as string,
|
newSubscription.customer as string,
|
||||||
@@ -136,8 +129,27 @@ export default async function handler(
|
|||||||
return res.status(400).send("Webhook Error: Admin not found");
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const newServersQuantity = admin.serversQuantity;
|
if (newSubscription.status === "active") {
|
||||||
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
|
await db
|
||||||
|
.update(admins)
|
||||||
|
.set({
|
||||||
|
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
eq(admins.stripeCustomerId, newSubscription.customer as string),
|
||||||
|
);
|
||||||
|
|
||||||
|
const newServersQuantity = admin.serversQuantity;
|
||||||
|
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
|
||||||
|
} else {
|
||||||
|
await disableServers(admin.adminId);
|
||||||
|
await db
|
||||||
|
.update(admins)
|
||||||
|
.set({ serversQuantity: 0 })
|
||||||
|
.where(
|
||||||
|
eq(admins.stripeCustomerId, newSubscription.customer as string),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -148,6 +160,13 @@ export default async function handler(
|
|||||||
newInvoice.subscription as string,
|
newInvoice.subscription as string,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (suscription.status !== "active") {
|
||||||
|
console.log(
|
||||||
|
`Skipping invoice.payment_succeeded for subscription ${suscription.id} with status ${suscription.status}`,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(admins)
|
.update(admins)
|
||||||
.set({
|
.set({
|
||||||
@@ -168,22 +187,29 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
case "invoice.payment_failed": {
|
case "invoice.payment_failed": {
|
||||||
const newInvoice = event.data.object as Stripe.Invoice;
|
const newInvoice = event.data.object as Stripe.Invoice;
|
||||||
await db
|
|
||||||
.update(admins)
|
|
||||||
.set({
|
|
||||||
serversQuantity: 0,
|
|
||||||
})
|
|
||||||
.where(eq(admins.stripeCustomerId, newInvoice.customer as string));
|
|
||||||
|
|
||||||
const admin = await findAdminByStripeCustomerId(
|
const subscription = await stripe.subscriptions.retrieve(
|
||||||
newInvoice.customer as string,
|
newInvoice.subscription as string,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!admin) {
|
if (subscription.status !== "active") {
|
||||||
return res.status(400).send("Webhook Error: Admin not found");
|
const admin = await findAdminByStripeCustomerId(
|
||||||
|
newInvoice.customer as string,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!admin) {
|
||||||
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
|
}
|
||||||
|
await db
|
||||||
|
.update(admins)
|
||||||
|
.set({
|
||||||
|
serversQuantity: 0,
|
||||||
|
})
|
||||||
|
.where(eq(admins.stripeCustomerId, newInvoice.customer as string));
|
||||||
|
|
||||||
|
await disableServers(admin.adminId);
|
||||||
}
|
}
|
||||||
|
|
||||||
await disableServers(admin.adminId);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { AddApplication } from "@/components/dashboard/project/add-application";
|
|||||||
import { AddCompose } from "@/components/dashboard/project/add-compose";
|
import { AddCompose } from "@/components/dashboard/project/add-compose";
|
||||||
import { AddDatabase } from "@/components/dashboard/project/add-database";
|
import { AddDatabase } from "@/components/dashboard/project/add-database";
|
||||||
import { AddTemplate } from "@/components/dashboard/project/add-template";
|
import { AddTemplate } from "@/components/dashboard/project/add-template";
|
||||||
|
import { ProjectEnviroment } from "@/components/dashboard/projects/project-enviroment";
|
||||||
import {
|
import {
|
||||||
MariadbIcon,
|
MariadbIcon,
|
||||||
MongodbIcon,
|
MongodbIcon,
|
||||||
@@ -198,27 +199,35 @@ const Project = (
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(auth?.rol === "admin" || user?.canCreateServices) && (
|
{(auth?.rol === "admin" || user?.canCreateServices) && (
|
||||||
<DropdownMenu>
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
<DropdownMenuTrigger asChild>
|
<ProjectEnviroment projectId={projectId}>
|
||||||
<Button>
|
<Button variant="outline">Project Enviroment</Button>
|
||||||
<PlusIcon className="h-4 w-4" />
|
</ProjectEnviroment>
|
||||||
Create Service
|
<DropdownMenu>
|
||||||
</Button>
|
<DropdownMenuTrigger asChild>
|
||||||
</DropdownMenuTrigger>
|
<Button>
|
||||||
<DropdownMenuContent className="w-[200px] space-y-2" align="end">
|
<PlusIcon className="h-4 w-4" />
|
||||||
<DropdownMenuLabel className="text-sm font-normal ">
|
Create Service
|
||||||
Actions
|
</Button>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuContent
|
||||||
<AddApplication
|
className="w-[200px] space-y-2"
|
||||||
projectId={projectId}
|
align="end"
|
||||||
projectName={data?.name}
|
>
|
||||||
/>
|
<DropdownMenuLabel className="text-sm font-normal ">
|
||||||
<AddDatabase projectId={projectId} projectName={data?.name} />
|
Actions
|
||||||
<AddCompose projectId={projectId} projectName={data?.name} />
|
</DropdownMenuLabel>
|
||||||
<AddTemplate projectId={projectId} />
|
<DropdownMenuSeparator />
|
||||||
</DropdownMenuContent>
|
<AddApplication
|
||||||
</DropdownMenu>
|
projectId={projectId}
|
||||||
|
projectName={data?.name}
|
||||||
|
/>
|
||||||
|
<AddDatabase projectId={projectId} projectName={data?.name} />
|
||||||
|
<AddCompose projectId={projectId} projectName={data?.name} />
|
||||||
|
<AddTemplate projectId={projectId} />
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,18 +1,29 @@
|
|||||||
import { ShowProjects } from "@/components/dashboard/projects/show";
|
import { ShowProjects } from "@/components/dashboard/projects/show";
|
||||||
import { ShowWelcomeDokploy } from "@/components/dashboard/settings/billing/show-welcome-dokploy";
|
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
|
||||||
|
const ShowWelcomeDokploy = dynamic(
|
||||||
|
() =>
|
||||||
|
import("@/components/dashboard/settings/billing/show-welcome-dokploy").then(
|
||||||
|
(mod) => mod.ShowWelcomeDokploy,
|
||||||
|
),
|
||||||
|
{ ssr: false },
|
||||||
|
);
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ShowWelcomeDokploy />
|
{isCloud && <ShowWelcomeDokploy />}
|
||||||
|
|
||||||
<ShowProjects />
|
<ShowProjects />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { AppearanceForm } from "@/components/dashboard/settings/appearance-form"
|
|||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
|
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
@@ -30,6 +31,7 @@ export async function getServerSideProps(
|
|||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req, res);
|
||||||
|
const locale = getLocale(req.cookies);
|
||||||
|
|
||||||
const helpers = createServerSideHelpers({
|
const helpers = createServerSideHelpers({
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
@@ -63,6 +65,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
trpcState: helpers.dehydrate(),
|
trpcState: helpers.dehydrate(),
|
||||||
|
...(await serverSideTranslations(locale, ["settings"])),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
|||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
@@ -41,6 +42,7 @@ export async function getServerSideProps(
|
|||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
|
const locale = getLocale(req.cookies);
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req, res);
|
||||||
|
|
||||||
const helpers = createServerSideHelpers({
|
const helpers = createServerSideHelpers({
|
||||||
@@ -75,6 +77,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
trpcState: helpers.dehydrate(),
|
trpcState: helpers.dehydrate(),
|
||||||
|
...(await serverSideTranslations(locale, ["settings"])),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { WebServer } from "@/components/dashboard/settings/web-server";
|
|||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
|
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
||||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
@@ -31,6 +32,7 @@ export async function getServerSideProps(
|
|||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
|
const locale = await getLocale(req.cookies);
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -73,6 +75,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
trpcState: helpers.dehydrate(),
|
trpcState: helpers.dehydrate(),
|
||||||
|
...(await serverSideTranslations(locale, ["settings"])),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ export default function Home({ IS_CLOUD }: Props) {
|
|||||||
) : (
|
) : (
|
||||||
<Link
|
<Link
|
||||||
className="hover:underline text-muted-foreground"
|
className="hover:underline text-muted-foreground"
|
||||||
href="https://docs.dokploy.com/docs/core/get-started/reset-password"
|
href="https://docs.dokploy.com/docs/core/reset-password"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Lost your password?
|
Lost your password?
|
||||||
|
|||||||
1
apps/dokploy/public/locales/de/common.json
Normal file
1
apps/dokploy/public/locales/de/common.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
44
apps/dokploy/public/locales/de/settings.json
Normal file
44
apps/dokploy/public/locales/de/settings.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"settings.common.save": "Speichern",
|
||||||
|
"settings.server.domain.title": "Server-Domain",
|
||||||
|
"settings.server.domain.description": "Füg eine Domain zu deiner Server-Anwendung hinzu.",
|
||||||
|
"settings.server.domain.form.domain": "Domain",
|
||||||
|
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt E-Mail",
|
||||||
|
"settings.server.domain.form.certificate.label": "Zertifikat",
|
||||||
|
"settings.server.domain.form.certificate.placeholder": "Wähl ein Zertifikat aus",
|
||||||
|
"settings.server.domain.form.certificateOptions.none": "Keins",
|
||||||
|
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Standard)",
|
||||||
|
|
||||||
|
"settings.server.webServer.title": "Web-Server",
|
||||||
|
"settings.server.webServer.description": "Lade den Web-Server neu oder reinige ihn.",
|
||||||
|
"settings.server.webServer.actions": "Aktionen",
|
||||||
|
"settings.server.webServer.reload": "Neu laden",
|
||||||
|
"settings.server.webServer.watchLogs": "Logs anschauen",
|
||||||
|
"settings.server.webServer.updateServerIp": "Server-IP Aktualisieren",
|
||||||
|
"settings.server.webServer.server.label": "Server",
|
||||||
|
"settings.server.webServer.traefik.label": "Traefik",
|
||||||
|
"settings.server.webServer.traefik.modifyEnv": "Umgebungsvariablen ändern",
|
||||||
|
"settings.server.webServer.storage.label": "Speicherplatz",
|
||||||
|
"settings.server.webServer.storage.cleanUnusedImages": "Nicht genutzte Bilder löschen",
|
||||||
|
"settings.server.webServer.storage.cleanUnusedVolumes": "Nicht genutzte Volumes löschen",
|
||||||
|
"settings.server.webServer.storage.cleanStoppedContainers": "Gestoppte Container löschen",
|
||||||
|
"settings.server.webServer.storage.cleanDockerBuilder": "Docker Builder & System bereinigen",
|
||||||
|
"settings.server.webServer.storage.cleanMonitoring": "Monitoring bereinigen",
|
||||||
|
"settings.server.webServer.storage.cleanAll": "Alles bereinigen",
|
||||||
|
|
||||||
|
"settings.profile.title": "Konto",
|
||||||
|
"settings.profile.description": "Ändere die Details deines Profiles hier.",
|
||||||
|
"settings.profile.email": "E-Mail",
|
||||||
|
"settings.profile.password": "Passwort",
|
||||||
|
"settings.profile.avatar": "Avatar",
|
||||||
|
|
||||||
|
"settings.appearance.title": "Aussehen",
|
||||||
|
"settings.appearance.description": "Pass das Design deines Dashboards an.",
|
||||||
|
"settings.appearance.theme": "Theme",
|
||||||
|
"settings.appearance.themeDescription": "Wähl ein Theme für dein Dashboard aus",
|
||||||
|
"settings.appearance.themes.light": "Hell",
|
||||||
|
"settings.appearance.themes.dark": "Dunkel",
|
||||||
|
"settings.appearance.themes.system": "System",
|
||||||
|
"settings.appearance.language": "Sprache",
|
||||||
|
"settings.appearance.languageDescription": "Wähl eine Sprache für dein Dashboard aus"
|
||||||
|
}
|
||||||
1
apps/dokploy/public/locales/en/common.json
Normal file
1
apps/dokploy/public/locales/en/common.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
44
apps/dokploy/public/locales/en/settings.json
Normal file
44
apps/dokploy/public/locales/en/settings.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"settings.common.save": "Save",
|
||||||
|
"settings.server.domain.title": "Server Domain",
|
||||||
|
"settings.server.domain.description": "Add a domain to your server application.",
|
||||||
|
"settings.server.domain.form.domain": "Domain",
|
||||||
|
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Email",
|
||||||
|
"settings.server.domain.form.certificate.label": "Certificate",
|
||||||
|
"settings.server.domain.form.certificate.placeholder": "Select a certificate",
|
||||||
|
"settings.server.domain.form.certificateOptions.none": "None",
|
||||||
|
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Default)",
|
||||||
|
|
||||||
|
"settings.server.webServer.title": "Web Server",
|
||||||
|
"settings.server.webServer.description": "Reload or clean the web server.",
|
||||||
|
"settings.server.webServer.actions": "Actions",
|
||||||
|
"settings.server.webServer.reload": "Reload",
|
||||||
|
"settings.server.webServer.watchLogs": "Watch logs",
|
||||||
|
"settings.server.webServer.updateServerIp": "Update Server IP",
|
||||||
|
"settings.server.webServer.server.label": "Server",
|
||||||
|
"settings.server.webServer.traefik.label": "Traefik",
|
||||||
|
"settings.server.webServer.traefik.modifyEnv": "Modify Env",
|
||||||
|
"settings.server.webServer.storage.label": "Space",
|
||||||
|
"settings.server.webServer.storage.cleanUnusedImages": "Clean unused images",
|
||||||
|
"settings.server.webServer.storage.cleanUnusedVolumes": "Clean unused volumes",
|
||||||
|
"settings.server.webServer.storage.cleanStoppedContainers": "Clean stopped containers",
|
||||||
|
"settings.server.webServer.storage.cleanDockerBuilder": "Clean Docker Builder & System",
|
||||||
|
"settings.server.webServer.storage.cleanMonitoring": "Clean Monitoring",
|
||||||
|
"settings.server.webServer.storage.cleanAll": "Clean all",
|
||||||
|
|
||||||
|
"settings.profile.title": "Account",
|
||||||
|
"settings.profile.description": "Change the details of your profile here.",
|
||||||
|
"settings.profile.email": "Email",
|
||||||
|
"settings.profile.password": "Password",
|
||||||
|
"settings.profile.avatar": "Avatar",
|
||||||
|
|
||||||
|
"settings.appearance.title": "Appearance",
|
||||||
|
"settings.appearance.description": "Customize the theme of your dashboard.",
|
||||||
|
"settings.appearance.theme": "Theme",
|
||||||
|
"settings.appearance.themeDescription": "Select a theme for your dashboard",
|
||||||
|
"settings.appearance.themes.light": "Light",
|
||||||
|
"settings.appearance.themes.dark": "Dark",
|
||||||
|
"settings.appearance.themes.system": "System",
|
||||||
|
"settings.appearance.language": "Language",
|
||||||
|
"settings.appearance.languageDescription": "Select a language for your dashboard"
|
||||||
|
}
|
||||||
1
apps/dokploy/public/locales/fa/common.json
Normal file
1
apps/dokploy/public/locales/fa/common.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
44
apps/dokploy/public/locales/fa/settings.json
Normal file
44
apps/dokploy/public/locales/fa/settings.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"settings.common.save": "ذخیره",
|
||||||
|
"settings.server.domain.title": "دامنه سرور",
|
||||||
|
"settings.server.domain.description": "یک دامنه به برنامه سرور خود اضافه کنید.",
|
||||||
|
"settings.server.domain.form.domain": "دامنه",
|
||||||
|
"settings.server.domain.form.letsEncryptEmail": "ایمیل Let's Encrypt",
|
||||||
|
"settings.server.domain.form.certificate.label": "گواهینامه",
|
||||||
|
"settings.server.domain.form.certificate.placeholder": "یک گواهینامه انتخاب کنید",
|
||||||
|
"settings.server.domain.form.certificateOptions.none": "هیچکدام",
|
||||||
|
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (پیشفرض)",
|
||||||
|
|
||||||
|
"settings.server.webServer.title": "وب سرور",
|
||||||
|
"settings.server.webServer.description": "وب سرور را بازنشانی یا پاک کنید.",
|
||||||
|
"settings.server.webServer.actions": "اقدامات",
|
||||||
|
"settings.server.webServer.reload": "بارگذاری مجدد",
|
||||||
|
"settings.server.webServer.watchLogs": "مشاهده گزارشها",
|
||||||
|
"settings.server.webServer.updateServerIp": "بهروزرسانی آیپی سرور",
|
||||||
|
"settings.server.webServer.server.label": "سرور",
|
||||||
|
"settings.server.webServer.traefik.label": "ترافیک",
|
||||||
|
"settings.server.webServer.traefik.modifyEnv": "ویرایش محیط",
|
||||||
|
"settings.server.webServer.storage.label": "فضا",
|
||||||
|
"settings.server.webServer.storage.cleanUnusedImages": "پاکسازی Image های بدون استفاده",
|
||||||
|
"settings.server.webServer.storage.cleanUnusedVolumes": "پاکسازی ولومهای بدون استفاده",
|
||||||
|
"settings.server.webServer.storage.cleanStoppedContainers": "پاکسازی کانتینرهای متوقفشده",
|
||||||
|
"settings.server.webServer.storage.cleanDockerBuilder": "پاکسازی بیلدر و سیستم داکر",
|
||||||
|
"settings.server.webServer.storage.cleanMonitoring": "پاکسازی پایش",
|
||||||
|
"settings.server.webServer.storage.cleanAll": "پاکسازی همه",
|
||||||
|
|
||||||
|
"settings.profile.title": "حساب کاربری",
|
||||||
|
"settings.profile.description": "جزئیات پروفایل خود را در اینجا تغییر دهید.",
|
||||||
|
"settings.profile.email": "ایمیل",
|
||||||
|
"settings.profile.password": "رمز عبور",
|
||||||
|
"settings.profile.avatar": "تصویر پروفایل",
|
||||||
|
|
||||||
|
"settings.appearance.title": "ظاهر",
|
||||||
|
"settings.appearance.description": "تم داشبورد خود را سفارشی کنید.",
|
||||||
|
"settings.appearance.theme": "تم",
|
||||||
|
"settings.appearance.themeDescription": "یک تم برای داشبورد خود انتخاب کنید",
|
||||||
|
"settings.appearance.themes.light": "روشن",
|
||||||
|
"settings.appearance.themes.dark": "تاریک",
|
||||||
|
"settings.appearance.themes.system": "سیستم",
|
||||||
|
"settings.appearance.language": "زبان",
|
||||||
|
"settings.appearance.languageDescription": "یک زبان برای داشبورد خود انتخاب کنید"
|
||||||
|
}
|
||||||
1
apps/dokploy/public/locales/fr/common.json
Normal file
1
apps/dokploy/public/locales/fr/common.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
44
apps/dokploy/public/locales/fr/settings.json
Normal file
44
apps/dokploy/public/locales/fr/settings.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"settings.common.save": "Sauvegarder",
|
||||||
|
"settings.server.domain.title": "Nom de domaine du serveur",
|
||||||
|
"settings.server.domain.description": "Ajouter un nom de domaine au serveur de votre application.",
|
||||||
|
"settings.server.domain.form.domain": "Domaine",
|
||||||
|
"settings.server.domain.form.letsEncryptEmail": "Adresse email Let's Encrypt",
|
||||||
|
"settings.server.domain.form.certificate.label": "Certificat",
|
||||||
|
"settings.server.domain.form.certificate.placeholder": "Choisir un certificat",
|
||||||
|
"settings.server.domain.form.certificateOptions.none": "Aucun",
|
||||||
|
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Par défaut)",
|
||||||
|
|
||||||
|
"settings.server.webServer.title": "Serveur web",
|
||||||
|
"settings.server.webServer.description": "Recharger ou nettoyer le serveur web.",
|
||||||
|
"settings.server.webServer.actions": "Actions",
|
||||||
|
"settings.server.webServer.reload": "Recharger",
|
||||||
|
"settings.server.webServer.watchLogs": "Consulter les logs",
|
||||||
|
"settings.server.webServer.updateServerIp": "Mettre à jour l'IP du serveur",
|
||||||
|
"settings.server.webServer.server.label": "Serveur",
|
||||||
|
"settings.server.webServer.traefik.label": "Traefik",
|
||||||
|
"settings.server.webServer.traefik.modifyEnv": "Modifier les variables d'environnement",
|
||||||
|
"settings.server.webServer.storage.label": "Stockage",
|
||||||
|
"settings.server.webServer.storage.cleanUnusedImages": "Supprimer les images inutilisées",
|
||||||
|
"settings.server.webServer.storage.cleanUnusedVolumes": "Supprimer les volumes inutilisés",
|
||||||
|
"settings.server.webServer.storage.cleanStoppedContainers": "Supprimer les conteneurs arrêtés",
|
||||||
|
"settings.server.webServer.storage.cleanDockerBuilder": "Nettoyer le Docker Builder & System",
|
||||||
|
"settings.server.webServer.storage.cleanMonitoring": "Nettoyer le monitoring",
|
||||||
|
"settings.server.webServer.storage.cleanAll": "Tout nettoyer",
|
||||||
|
|
||||||
|
"settings.profile.title": "Compte",
|
||||||
|
"settings.profile.description": "Modifier les informations de votre compte ici.",
|
||||||
|
"settings.profile.email": "Adresse Email",
|
||||||
|
"settings.profile.password": "Mot de passe",
|
||||||
|
"settings.profile.avatar": "Photo de profil",
|
||||||
|
|
||||||
|
"settings.appearance.title": "Apparence",
|
||||||
|
"settings.appearance.description": "Customiser le thème de votre dashboard.",
|
||||||
|
"settings.appearance.theme": "Thème",
|
||||||
|
"settings.appearance.themeDescription": "Choisir un thème pour votre dashboard",
|
||||||
|
"settings.appearance.themes.light": "Clair",
|
||||||
|
"settings.appearance.themes.dark": "Sombre",
|
||||||
|
"settings.appearance.themes.system": "Système",
|
||||||
|
"settings.appearance.language": "Langue",
|
||||||
|
"settings.appearance.languageDescription": "Sélectionner une langue pour votre dashboard"
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user