mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-03 04:55:23 +02:00
Compare commits
470 Commits
v0.23.2
...
feat/concu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2da2b2dd39 | ||
|
|
7273c636a0 | ||
|
|
d6a0585bae | ||
|
|
935d1686f2 | ||
|
|
349248105a | ||
|
|
d922568510 | ||
|
|
44ae4df151 | ||
|
|
77fdda4c09 | ||
|
|
8a1e36cc3b | ||
|
|
1635bab44f | ||
|
|
4a52459015 | ||
|
|
17f333ac2a | ||
|
|
d770307d64 | ||
|
|
aa434cbdea | ||
|
|
c42054b965 | ||
|
|
03588bf375 | ||
|
|
8c420ff4f5 | ||
|
|
cbf6f95891 | ||
|
|
2d2a3d74ec | ||
|
|
56b9fb531a | ||
|
|
59aaa1a47a | ||
|
|
5e4444610c | ||
|
|
34e6cd87df | ||
|
|
31b13b8d34 | ||
|
|
746cf76cf3 | ||
|
|
46c53a05bf | ||
|
|
f97f6d8178 | ||
|
|
c653dd604f | ||
|
|
40877e4370 | ||
|
|
65203036f2 | ||
|
|
2ef5f967a9 | ||
|
|
b20c95ffbc | ||
|
|
09b2492585 | ||
|
|
ca1fa7c4f7 | ||
|
|
112b898d98 | ||
|
|
8185482bcd | ||
|
|
dd8f5dba09 | ||
|
|
e72a468c7e | ||
|
|
02dd793dfb | ||
|
|
64ef033950 | ||
|
|
32f7bdf398 | ||
|
|
8d73b77a19 | ||
|
|
2e3d4f1021 | ||
|
|
ba1f4dbd3a | ||
|
|
653beac3d9 | ||
|
|
37c34fdadc | ||
|
|
69d676178f | ||
|
|
6612c92b4f | ||
|
|
88c8fe4614 | ||
|
|
623fc26de5 | ||
|
|
220576fd63 | ||
|
|
07c23292da | ||
|
|
72fca80047 | ||
|
|
1e7f614bb6 | ||
|
|
e2662a0ec5 | ||
|
|
c96c25ca9f | ||
|
|
4afd2d11fa | ||
|
|
ff20bb2731 | ||
|
|
8a802b0739 | ||
|
|
e511173283 | ||
|
|
763b1a344a | ||
|
|
a4041185f1 | ||
|
|
522dcd6c08 | ||
|
|
b4d5935875 | ||
|
|
8cc054389a | ||
|
|
58b78e1ee3 | ||
|
|
5b0a8fde9c | ||
|
|
7e641c0ed5 | ||
|
|
83531ceacd | ||
|
|
6d9a1db8af | ||
|
|
e3979d2c48 | ||
|
|
5f39dfee3a | ||
|
|
774365c68e | ||
|
|
0cdacb5501 | ||
|
|
e266b1a620 | ||
|
|
81ab71ba23 | ||
|
|
ae92725554 | ||
|
|
974d1d8b26 | ||
|
|
7367601e26 | ||
|
|
861b390707 | ||
|
|
4e7378371d | ||
|
|
b6cbf9127d | ||
|
|
661c517dfc | ||
|
|
f3ec01ec77 | ||
|
|
f4499463fe | ||
|
|
6e069154ef | ||
|
|
66e6b56053 | ||
|
|
6248abd88e | ||
|
|
2c591cbd03 | ||
|
|
3864c50deb | ||
|
|
83e8c82c4a | ||
|
|
a41137aacc | ||
|
|
213acd6287 | ||
|
|
0781336b8f | ||
|
|
28811ca66d | ||
|
|
ed53bdd0fa | ||
|
|
957d1b5966 | ||
|
|
ad359defae | ||
|
|
a4bbcea282 | ||
|
|
15e62961e8 | ||
|
|
429c1e4cd8 | ||
|
|
1904a3d1e9 | ||
|
|
f0278f354b | ||
|
|
9763dce045 | ||
|
|
ef6dcaf363 | ||
|
|
37b056cd4b | ||
|
|
8bbef02e39 | ||
|
|
231b8ed19d | ||
|
|
cfa0135932 | ||
|
|
85bce827eb | ||
|
|
1fe12ba93e | ||
|
|
4b1146ab6b | ||
|
|
fe2f6f842b | ||
|
|
c3f29c2694 | ||
|
|
d5307cb5d6 | ||
|
|
b99556b389 | ||
|
|
112a1dedec | ||
|
|
edbdc01a1e | ||
|
|
883e1d1bfe | ||
|
|
33d6c2073b | ||
|
|
33873ce1e9 | ||
|
|
1d94c85c2b | ||
|
|
1758655f66 | ||
|
|
029eed7755 | ||
|
|
f017536396 | ||
|
|
ba5505cf81 | ||
|
|
5ff5da9ff9 | ||
|
|
e96a8ea4ad | ||
|
|
42864d2472 | ||
|
|
ae25ea265c | ||
|
|
0755f28307 | ||
|
|
3d10d48425 | ||
|
|
6e79183f6a | ||
|
|
2318fb062a | ||
|
|
14b4bc9d85 | ||
|
|
4c72f1894c | ||
|
|
ebd632df04 | ||
|
|
4878ed2b6f | ||
|
|
1d3ab2bafa | ||
|
|
7cb7cfa2a8 | ||
|
|
6009697710 | ||
|
|
6be86b49bb | ||
|
|
1e81244e0b | ||
|
|
f659ea463d | ||
|
|
22c7c6e6fb | ||
|
|
caf57276a4 | ||
|
|
15c6c7e657 | ||
|
|
8be0db385a | ||
|
|
4259e2533e | ||
|
|
a138d12082 | ||
|
|
a2405ddd84 | ||
|
|
e785ad5599 | ||
|
|
cc6445a8ec | ||
|
|
b8f27d7b76 | ||
|
|
32f61b5e9b | ||
|
|
e2d6b5eb8a | ||
|
|
7413c9484a | ||
|
|
607c505c4b | ||
|
|
42629e83a1 | ||
|
|
b9f18cddf7 | ||
|
|
2790895642 | ||
|
|
c21c88d89f | ||
|
|
bf6b9c6893 | ||
|
|
e08fe1dbea | ||
|
|
0b9eaac390 | ||
|
|
5ed49a5ca1 | ||
|
|
1300a6242c | ||
|
|
201f07c084 | ||
|
|
c5161f1612 | ||
|
|
0755de03c2 | ||
|
|
f376ea5fec | ||
|
|
346eb24926 | ||
|
|
fe45c69939 | ||
|
|
39d46a51b3 | ||
|
|
3e193590cc | ||
|
|
c157a353f3 | ||
|
|
2a14ae0c7f | ||
|
|
144c74e7f7 | ||
|
|
1d4d766f3a | ||
|
|
025d439f71 | ||
|
|
8532cba638 | ||
|
|
fdb4b176cb | ||
|
|
f2b214f8f0 | ||
|
|
0bcc59f90f | ||
|
|
7ae4bf3215 | ||
|
|
0f5cf37757 | ||
|
|
a7bde655da | ||
|
|
295b6df5e1 | ||
|
|
b5b63eae4f | ||
|
|
794e03460f | ||
|
|
e8f36f8ba5 | ||
| f9210d3165 | |||
|
|
9bc6411c98 | ||
| f8261b5364 | |||
| 30c2c7afb0 | |||
|
|
f26c1c0da6 | ||
|
|
d02976476a | ||
|
|
17e9154887 | ||
|
|
2442494096 | ||
|
|
bac2afb423 | ||
|
|
4e9630e976 | ||
|
|
558f6aecae | ||
|
|
9baafb83ff | ||
|
|
c3e2b0d0f1 | ||
|
|
11d584316a | ||
|
|
f78dc555b2 | ||
|
|
5812b12a59 | ||
|
|
7301d15e8f | ||
|
|
f79796a6c8 | ||
|
|
4122b37abd | ||
|
|
79e9593663 | ||
|
|
def3fa0030 | ||
|
|
d561068bcd | ||
|
|
212c1b2d5f | ||
|
|
d3a54172b5 | ||
|
|
1f9ef473f1 | ||
|
|
a0bbf7be23 | ||
|
|
a5bc384d77 | ||
|
|
cda33eb291 | ||
|
|
c178234e53 | ||
|
|
f2ae39aa86 | ||
|
|
329db1fd1a | ||
|
|
6efbf030a7 | ||
|
|
b95dfed8fc | ||
|
|
7fe3418d55 | ||
|
|
288d86c73b | ||
|
|
ffd5ccd386 | ||
|
|
98ddd096e5 | ||
|
|
da6cc9fe72 | ||
|
|
22d0af269e | ||
|
|
f0fdc46de5 | ||
|
|
9aea24115d | ||
|
|
a9ee6c2393 | ||
|
|
349717044c | ||
|
|
f94f32695f | ||
|
|
37b78ea09c | ||
|
|
9b89b4631f | ||
|
|
7100095f2b | ||
|
|
a36ab65aa6 | ||
|
|
bf81ba20ff | ||
|
|
658a4a9b99 | ||
|
|
47cb096cf3 | ||
|
|
f3856722da | ||
|
|
a67c3eb979 | ||
|
|
aaa205f104 | ||
|
|
cadea7ff28 | ||
|
|
9ab937f726 | ||
|
|
d0af517eb7 | ||
|
|
66bdf9bf0a | ||
|
|
d4a3af475a | ||
|
|
e92a8d7c98 | ||
|
|
c4fec8cee5 | ||
|
|
55f75bce53 | ||
|
|
fdc524d79d | ||
|
|
93d6662466 | ||
|
|
1977235d31 | ||
|
|
1dd713a1d1 | ||
|
|
18b65f28f2 | ||
|
|
666db23b8e | ||
|
|
2ca5321fdc | ||
|
|
3f3ff9670b | ||
|
|
7fb902551e | ||
|
|
a201b3f979 | ||
|
|
01d78e50fc | ||
|
|
6681ba7bbd | ||
|
|
0b71411c0e | ||
|
|
19f7465910 | ||
|
|
f33dd37571 | ||
|
|
a0031ed07f | ||
|
|
2ca4e264c4 | ||
|
|
fa81d04fb3 | ||
|
|
bd8745393b | ||
|
|
691c83c256 | ||
|
|
6bd85e9216 | ||
|
|
79c29fa92d | ||
|
|
89f71fe889 | ||
|
|
bddafe294d | ||
|
|
94829daf15 | ||
|
|
2209d44ea5 | ||
|
|
b12c035527 | ||
|
|
baadba542f | ||
|
|
a8fc052cbf | ||
|
|
fa5994bd47 | ||
|
|
96d0810607 | ||
|
|
2d382ea1be | ||
|
|
d78974efc0 | ||
|
|
81040c899f | ||
|
|
c7344190b4 | ||
|
|
257c0eb106 | ||
|
|
c03b9509c8 | ||
|
|
d87205c4dc | ||
|
|
48aef798e4 | ||
|
|
baa5cd5c58 | ||
|
|
5aae36996e | ||
|
|
ec8fa9fefe | ||
|
|
d959f59c2d | ||
|
|
a1169795e4 | ||
|
|
10af7925db | ||
|
|
c64cdca2e8 | ||
|
|
a5b95d8cf3 | ||
|
|
78b60f7d8a | ||
|
|
58e6a14cd6 | ||
|
|
0aac6da554 | ||
|
|
978c4d85c5 | ||
|
|
70e08c96eb | ||
|
|
027853a361 | ||
|
|
43ebe4dc7c | ||
|
|
0113ebe7da | ||
|
|
c36b40aa29 | ||
|
|
caea934f88 | ||
|
|
9b2ea1cade | ||
|
|
3a82c4b27b | ||
|
|
22a26e9873 | ||
|
|
226a287ce7 | ||
|
|
320b927aac | ||
|
|
d799b460bd | ||
|
|
3ed9da147b | ||
|
|
636dec4f09 | ||
|
|
4dcf6cf4c3 | ||
|
|
7356d71626 | ||
|
|
76603f598c | ||
|
|
e050c218e2 | ||
|
|
46e0b5df75 | ||
|
|
5b36503a3f | ||
|
|
b9afc551db | ||
|
|
078ca19578 | ||
|
|
b7dc7bbf0c | ||
|
|
b9ee81aa59 | ||
|
|
b2d01a2889 | ||
|
|
5ec3d63ab2 | ||
|
|
f0ef06ed8c | ||
|
|
75b2c34a13 | ||
|
|
cd4533df9e | ||
|
|
65a3d8175a | ||
|
|
d3702d22f2 | ||
|
|
d4b74c54da | ||
|
|
80ede659fb | ||
|
|
c59ea57814 | ||
|
|
381186c9f1 | ||
|
|
05e0031daf | ||
|
|
1bacd42bf5 | ||
|
|
2bc12a20ba | ||
|
|
1943a9e8fa | ||
|
|
db9109a3be | ||
|
|
2f6084ec8f | ||
|
|
a62c9f63e1 | ||
|
|
ee2bbf5e37 | ||
|
|
d27dff4906 | ||
|
|
7874445510 | ||
|
|
5c73ced500 | ||
|
|
b1450d14ac | ||
|
|
c5e3b00990 | ||
|
|
cf3f44f686 | ||
|
|
119883e746 | ||
|
|
2918868166 | ||
|
|
715e44116d | ||
|
|
c15ee721ff | ||
|
|
71befc3a4b | ||
|
|
bca32f077b | ||
|
|
1afcecaec0 | ||
|
|
80b72dc75e | ||
|
|
5973d7b9b8 | ||
|
|
ab3a1504cf | ||
|
|
f51c51958f | ||
|
|
f3703e6f5e | ||
|
|
14cb6cecae | ||
|
|
6885b140eb | ||
|
|
8e8712e33d | ||
|
|
56fcaa8ccd | ||
|
|
d229284e5e | ||
|
|
3561b5cae6 | ||
|
|
a3192d6584 | ||
|
|
24ea8b7fbd | ||
|
|
e12df7b32e | ||
|
|
8001c9cfc2 | ||
|
|
a762b4b4ae | ||
|
|
9cfbd664c5 | ||
|
|
f9972bee60 | ||
|
|
3970cd452b | ||
|
|
6fc51e02a7 | ||
|
|
fa7db0dc75 | ||
|
|
107cdcee49 | ||
|
|
6521491e2f | ||
|
|
c5311f2a9f | ||
|
|
7726c8db21 | ||
|
|
b272f01a18 | ||
|
|
bddb07898e | ||
|
|
2fe7349889 | ||
|
|
5c1c969873 | ||
|
|
2f6f1b19e7 | ||
|
|
934ec9b16a | ||
|
|
c6d760a904 | ||
|
|
4f021a3f79 | ||
|
|
0c861585ed | ||
|
|
d15ccfe505 | ||
|
|
e158e05ad6 | ||
|
|
e21605030a | ||
|
|
f1c46f0d19 | ||
|
|
fbf3776548 | ||
|
|
46d84eaa71 | ||
|
|
2a2b947998 | ||
|
|
9974b2326f | ||
|
|
c042c8c0c5 | ||
|
|
819a310d48 | ||
|
|
12860a0736 | ||
|
|
392e2d66ec | ||
|
|
1f4ce2daf3 | ||
|
|
49edf17463 | ||
|
|
6eea02c098 | ||
|
|
a5bba9a11b | ||
|
|
37c7507507 | ||
|
|
ce88a0a5f2 | ||
|
|
40f28705cb | ||
|
|
03e04b7bce | ||
|
|
b920e7c0f1 | ||
|
|
15fde820a3 | ||
|
|
64293fce79 | ||
|
|
526f249d0e | ||
|
|
17c5a42d8e | ||
|
|
fac96b5db5 | ||
|
|
4796b0cf4e | ||
|
|
159a055bc6 | ||
|
|
cfade317f1 | ||
|
|
36ebefff16 | ||
|
|
7cc0603078 | ||
|
|
e0e42ac554 | ||
|
|
e004d8bd52 | ||
|
|
0c0912f606 | ||
|
|
8d81440d9b | ||
|
|
9ede3bd71b | ||
|
|
df6a72ea50 | ||
|
|
e79f8c4b72 | ||
|
|
26e2a24f63 | ||
|
|
830feabd70 | ||
|
|
122a3d110d | ||
|
|
c05edb313f | ||
|
|
1ec2853862 | ||
|
|
5c2709248c | ||
|
|
bc79074441 | ||
|
|
3b5428697b | ||
|
|
5ada451916 | ||
|
|
6b0d9240dd | ||
|
|
475c452451 | ||
|
|
1e31ebb9c2 | ||
|
|
207fe6f477 | ||
|
|
0b34676336 | ||
|
|
499022a328 | ||
|
|
fb5d2bd5b6 | ||
|
|
e42f6bc610 | ||
|
|
61cf426615 | ||
|
|
dde12e132a | ||
|
|
fd0f679d0f | ||
|
|
2a89be6efc | ||
|
|
412bb9e874 | ||
|
|
6290c217f1 | ||
|
|
4babdd45ea | ||
|
|
24bff96898 | ||
|
|
df8f1252a0 | ||
|
|
8599f519a4 | ||
|
|
113e4ae4b5 | ||
|
|
7f0bdc7e00 | ||
|
|
b685a817fd | ||
|
|
6061a443d1 | ||
|
|
4c9835d1f3 | ||
|
|
f2671f9369 | ||
|
|
bb904bb011 | ||
|
|
9fb6ca2b3b | ||
|
|
9f146d7d80 | ||
|
|
cad628d155 | ||
|
|
cd8230b0e5 |
21
.github/pull_request_template.md
vendored
Normal file
21
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
## What is this PR about?
|
||||||
|
|
||||||
|
Please describe in a short paragraph what this PR is about.
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
Before submitting this PR, please make sure that:
|
||||||
|
|
||||||
|
- [ ] You created a dedicated branch based on the `canary` branch.
|
||||||
|
- [ ] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request
|
||||||
|
- [ ] You have tested this PR in your local instance.
|
||||||
|
|
||||||
|
## Issues related (if applicable)
|
||||||
|
|
||||||
|
Close automatically the related issues using the keywords: `closes #ISSUE_NUMBER`, `fixes #ISSUE_NUMBER`, `resolves #ISSUE_NUMBER`
|
||||||
|
|
||||||
|
Example: `closes #123`
|
||||||
|
|
||||||
|
## Screenshots (if applicable)
|
||||||
|
|
||||||
|
If you include a video or screenshot, would be awesome so we can see the changes in action.
|
||||||
4
.github/workflows/create-pr.yml
vendored
4
.github/workflows/create-pr.yml
vendored
@@ -19,17 +19,14 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Get version from package.json
|
- name: Get version from package.json
|
||||||
id: package_version
|
|
||||||
run: echo "VERSION=$(jq -r .version ./apps/dokploy/package.json)" >> $GITHUB_ENV
|
run: echo "VERSION=$(jq -r .version ./apps/dokploy/package.json)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Get latest GitHub tag
|
- name: Get latest GitHub tag
|
||||||
id: latest_tag
|
|
||||||
run: |
|
run: |
|
||||||
LATEST_TAG=$(git ls-remote --tags origin | awk -F'/' '{print $3}' | sort -V | tail -n1)
|
LATEST_TAG=$(git ls-remote --tags origin | awk -F'/' '{print $3}' | sort -V | tail -n1)
|
||||||
echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV
|
echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV
|
||||||
echo $LATEST_TAG
|
echo $LATEST_TAG
|
||||||
- name: Compare versions
|
- name: Compare versions
|
||||||
id: compare_versions
|
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ env.VERSION }}" != "${{ env.LATEST_TAG }}" ]; then
|
if [ "${{ env.VERSION }}" != "${{ env.LATEST_TAG }}" ]; then
|
||||||
VERSION_CHANGED="true"
|
VERSION_CHANGED="true"
|
||||||
@@ -42,7 +39,6 @@ jobs:
|
|||||||
echo "Latest tag: ${{ env.LATEST_TAG }}"
|
echo "Latest tag: ${{ env.LATEST_TAG }}"
|
||||||
echo "Version changed: $VERSION_CHANGED"
|
echo "Version changed: $VERSION_CHANGED"
|
||||||
- name: Check if a PR already exists
|
- name: Check if a PR already exists
|
||||||
id: check_pr
|
|
||||||
run: |
|
run: |
|
||||||
PR_EXISTS=$(gh pr list --state open --base main --head canary --json number --jq '. | length')
|
PR_EXISTS=$(gh pr list --state open --base main --head canary --json number --jq '. | length')
|
||||||
echo "PR_EXISTS=$PR_EXISTS" >> $GITHUB_ENV
|
echo "PR_EXISTS=$PR_EXISTS" >> $GITHUB_ENV
|
||||||
|
|||||||
3
.github/workflows/deploy.yml
vendored
3
.github/workflows/deploy.yml
vendored
@@ -2,7 +2,8 @@ name: Build Docker images
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ["canary", "main", "feat/monitoring"]
|
branches: [main, canary]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push-cloud-image:
|
build-and-push-cloud-image:
|
||||||
|
|||||||
3
.github/workflows/dokploy.yml
vendored
3
.github/workflows/dokploy.yml
vendored
@@ -2,7 +2,8 @@ name: Dokploy Docker Build
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, canary, "1061-custom-docker-service-hostname"]
|
branches: [main, canary, "fix/re-apply-database-migration-fix"]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE_NAME: dokploy/dokploy
|
IMAGE_NAME: dokploy/dokploy
|
||||||
|
|||||||
6
.github/workflows/format.yml
vendored
6
.github/workflows/format.yml
vendored
@@ -11,12 +11,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup biomeJs
|
- name: Setup biomeJs
|
||||||
uses: biomejs/setup-biome@v2
|
uses: biomejs/setup-biome@v2
|
||||||
|
|
||||||
- name: Run Biome formatter
|
- name: Run Biome formatter
|
||||||
run: biome format . --write
|
run: biome format --write
|
||||||
|
|
||||||
- uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef
|
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 # v1.3.2
|
||||||
|
|||||||
39
.github/workflows/pull-request.yml
vendored
39
.github/workflows/pull-request.yml
vendored
@@ -4,9 +4,15 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, canary]
|
branches: [main, canary]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint-and-typecheck:
|
pr-check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
job: [build, test, typecheck]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
@@ -15,32 +21,5 @@ jobs:
|
|||||||
node-version: 20.16.0
|
node-version: 20.16.0
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm run server:build
|
- run: pnpm server:build
|
||||||
- run: pnpm typecheck
|
- run: pnpm ${{ matrix.job }}
|
||||||
|
|
||||||
build-and-test:
|
|
||||||
needs: lint-and-typecheck
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20.16.0
|
|
||||||
cache: "pnpm"
|
|
||||||
- run: pnpm install --frozen-lockfile
|
|
||||||
- run: pnpm run server:build
|
|
||||||
- run: pnpm build
|
|
||||||
|
|
||||||
parallel-tests:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20.16.0
|
|
||||||
cache: "pnpm"
|
|
||||||
- run: pnpm install --frozen-lockfile
|
|
||||||
- run: pnpm run server:build
|
|
||||||
- run: pnpm test
|
|
||||||
|
|||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["biomejs.biome"]
|
||||||
|
}
|
||||||
8
.vscode/settings.json
vendored
Normal file
8
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "biomejs.biome",
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.biome": "explicit",
|
||||||
|
"source.organizeImports.biome": "explicit"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -87,7 +87,8 @@ pnpm run dokploy:dev
|
|||||||
|
|
||||||
Go to http://localhost:3000 to see the development server
|
Go to http://localhost:3000 to see the development server
|
||||||
|
|
||||||
Note: this project uses Biome. If your editor is configured to use another formatter such as Prettier, it's recommended to either change it to use Biome or turn it off.
|
> [!NOTE]
|
||||||
|
> This project uses Biome. If your editor is configured to use another formatter such as Prettier, it's recommended to either change it to use Biome or turn it off.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
@@ -117,10 +118,10 @@ In the case you lost your password, you can reset it using the following command
|
|||||||
pnpm run reset-password
|
pnpm run reset-password
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to test the webhooks on development mode using localtunnel, make sure to install `localtunnel`
|
If you want to test the webhooks on development mode using localtunnel, make sure to install [`localtunnel`](https://localtunnel.app/)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bunx lt --port 3000
|
pnpm dlx localtunnel --port 3000
|
||||||
```
|
```
|
||||||
|
|
||||||
If you run into permission issues of docker run the following command
|
If you run into permission issues of docker run the following command
|
||||||
@@ -152,7 +153,7 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.
|
|||||||
|
|
||||||
## Pull Request
|
## Pull Request
|
||||||
|
|
||||||
- The `main` branch is the source of truth and should always reflect the latest stable release.
|
- The `canary` branch is the source of truth and should always reflect the latest stable release.
|
||||||
- Create a new branch for each feature or bug fix.
|
- Create a new branch for each feature or bug fix.
|
||||||
- Make sure to add tests for your changes.
|
- Make sure to add tests for your changes.
|
||||||
- Make sure to update the documentation for any changes Go to the [docs.dokploy.com](https://docs.dokploy.com) website to see the changes.
|
- Make sure to update the documentation for any changes Go to the [docs.dokploy.com](https://docs.dokploy.com) website to see the changes.
|
||||||
@@ -161,6 +162,12 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.
|
|||||||
- If your pull request fixes an open issue, please reference the issue in the pull request description.
|
- If your pull request fixes an open issue, please reference the issue in the pull request description.
|
||||||
- Once your pull request is merged, you will be automatically added as a contributor to the project.
|
- Once your pull request is merged, you will be automatically added as a contributor to the project.
|
||||||
|
|
||||||
|
**Important Considerations for Pull Requests:**
|
||||||
|
|
||||||
|
- **Focus and Scope:** Each Pull Request should ideally address a single, well-defined problem or introduce one new feature. This greatly facilitates review and reduces the chances of introducing unintended side effects.
|
||||||
|
- **Avoid Unfocused Changes:** Please avoid submitting Pull Requests that contain only minor changes such as whitespace adjustments, IDE-generated formatting, or removal of unused variables, unless these are part of a larger, clearly defined refactor or a dedicated "cleanup" Pull Request that addresses a specific `good first issue` or maintenance task.
|
||||||
|
- **Issue Association:** For any significant change, it's highly recommended to open an issue first to discuss the proposed solution with the community and maintainers. This ensures alignment and avoids duplicated effort. If your PR resolves an existing issue, please link it in the description (e.g., `Fixes #123`, `Closes #456`).
|
||||||
|
|
||||||
Thank you for your contribution!
|
Thank you for your contribution!
|
||||||
|
|
||||||
## Templates
|
## Templates
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
FROM node:20.9-slim AS base
|
FROM node:20.16.0-slim AS base
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
RUN corepack prepare pnpm@9.12.0 --activate
|
||||||
|
|
||||||
FROM base AS build
|
FROM base AS build
|
||||||
COPY . /usr/src/app
|
COPY . /usr/src/app
|
||||||
@@ -57,7 +58,7 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
|||||||
&& pnpm install -g tsx
|
&& pnpm install -g tsx
|
||||||
|
|
||||||
# Install Railpack
|
# Install Railpack
|
||||||
ARG RAILPACK_VERSION=0.0.64
|
ARG RAILPACK_VERSION=0.2.2
|
||||||
RUN curl -sSL https://railpack.com/install.sh | bash
|
RUN curl -sSL https://railpack.com/install.sh | bash
|
||||||
|
|
||||||
# Install buildpacks
|
# Install buildpacks
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
FROM node:20.9-slim AS base
|
FROM node:20.16.0-slim AS base
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
RUN corepack prepare pnpm@9.12.0 --activate
|
||||||
|
|
||||||
FROM base AS build
|
FROM base AS build
|
||||||
COPY . /usr/src/app
|
COPY . /usr/src/app
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
FROM node:20.9-slim AS base
|
FROM node:20.16.0-slim AS base
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
RUN corepack prepare pnpm@9.12.0 --activate
|
||||||
|
|
||||||
FROM base AS build
|
FROM base AS build
|
||||||
COPY . /usr/src/app
|
COPY . /usr/src/app
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
FROM node:20.9-slim AS base
|
FROM node:20.16.0-slim AS base
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
RUN corepack prepare pnpm@9.12.0 --activate
|
||||||
|
|
||||||
FROM base AS build
|
FROM base AS build
|
||||||
COPY . /usr/src/app
|
COPY . /usr/src/app
|
||||||
|
|||||||
23
GUIDES.md
23
GUIDES.md
@@ -16,28 +16,29 @@ Here's how to install docker on different operating systems:
|
|||||||
### Ubuntu
|
### Ubuntu
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Uninstall old versions
|
||||||
|
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done
|
||||||
|
|
||||||
# Update package index
|
# Update package index
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
|
|
||||||
# Install prerequisites
|
# Install prerequisites
|
||||||
sudo apt-get install \
|
sudo apt-get install ca-certificates curl
|
||||||
apt-transport-https \
|
sudo install -m 0755 -d /etc/apt/keyrings
|
||||||
ca-certificates \
|
|
||||||
curl \
|
|
||||||
gnupg \
|
|
||||||
lsb-release
|
|
||||||
|
|
||||||
# Add Docker's official GPG key
|
# Add Docker's official GPG key
|
||||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
||||||
|
sudo chmod a+r /etc/apt/keyrings/docker.asc
|
||||||
|
|
||||||
# Set up stable repository
|
# Add the repository to Apt sources
|
||||||
echo \
|
echo \
|
||||||
"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
|
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
|
||||||
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
|
||||||
|
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
|
||||||
# Install Docker Engine
|
# Install Docker Engine
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install docker-ce docker-ce-cli containerd.io
|
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
```
|
```
|
||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Core License (Apache License 2.0)
|
## Core License (Apache License 2.0)
|
||||||
|
|
||||||
Copyright 2024 Mauricio Siu.
|
Copyright 2025 Mauricio Siu.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
111
README.md
111
README.md
@@ -1,23 +1,19 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<div>
|
<a href="https://dokploy.com">
|
||||||
<a href="https://dokploy.com" target="_blank" rel="noopener">
|
<img src=".github/sponsors/logo.png" alt="Dokploy - Open Source Alternative to Vercel, Heroku and Netlify." width="100%" />
|
||||||
<img style="object-fit: cover;" align="center" width="100%"src=".github/sponsors/logo.png" alt="Dokploy - Open Source Alternative to Vercel, Heroku and Netlify." />
|
</a>
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</br>
|
|
||||||
<div align="center">
|
|
||||||
<div>Join us on Discord for help, feedback, and discussions!</div>
|
|
||||||
</br>
|
</br>
|
||||||
|
</br>
|
||||||
|
<p>Join us on Discord for help, feedback, and discussions!</p>
|
||||||
<a href="https://discord.gg/2tBnJ3jDJc">
|
<a href="https://discord.gg/2tBnJ3jDJc">
|
||||||
<img src="https://discordapp.com/api/guilds/1234073262418563112/widget.png?style=banner2" alt="Discord Shield"/>
|
<img src="https://discordapp.com/api/guilds/1234073262418563112/widget.png?style=banner2" alt="Discord Shield"/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
|
Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
|
||||||
|
|
||||||
### Features
|
## ✨ Features
|
||||||
|
|
||||||
Dokploy includes multiple features to make your life easier.
|
Dokploy includes multiple features to make your life easier.
|
||||||
|
|
||||||
@@ -47,7 +43,7 @@ curl -sSL https://dokploy.com/install.sh | sh
|
|||||||
|
|
||||||
For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||||
|
|
||||||
## Sponsors
|
## ♥️ Sponsors
|
||||||
|
|
||||||
🙏 We're deeply grateful to all our sponsors who make Dokploy possible! Your support helps cover the costs of hosting, testing, and developing new features.
|
🙏 We're deeply grateful to all our sponsors who make Dokploy possible! Your support helps cover the costs of hosting, testing, and developing new features.
|
||||||
|
|
||||||
@@ -61,76 +57,47 @@ 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>
|
||||||
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 10px;">
|
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy"><img src=".github/sponsors/hostinger.jpg" alt="Hostinger" width="300"/></a>
|
||||||
<img src=".github/sponsors/hostinger.jpg" alt="Hostinger" height="50"/>
|
<a href="https://www.lxaer.com/?ref=dokploy"><img src=".github/sponsors/lxaer.png" alt="LX Aer" width="100"/></a>
|
||||||
</a>
|
|
||||||
<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"/>
|
|
||||||
</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>
|
|
||||||
<a href="https://lightnode.com/?ref=dokploy" target="_blank" style="display: inline-block;">
|
|
||||||
<img src=".github/sponsors/light-node.webp" alt="Lightnode" height="70"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Premium Supporters 🥇 -->
|
||||||
|
|
||||||
|
<!-- Add Premium Supporters here -->
|
||||||
|
|
||||||
### Premium Supporters 🥇
|
### Premium Supporters 🥇
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; gap: 20px;">
|
<div>
|
||||||
<a href="https://supafort.com/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 20px;">
|
<a href="https://supafort.com/?ref=dokploy"><img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" width="300"/></a>
|
||||||
<img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" height="50"/>
|
<a href="https://agentdock.ai/?ref=dokploy"><img src=".github/sponsors/agentdock.png" alt="agentdock.ai" width="100"/></a>
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="https://agentdock.ai/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 50px;">
|
|
||||||
<img src=".github/sponsors/agentdock.png" alt="agentdock.ai" height="70"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### Elite Contributors 🥈
|
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; gap: 20px;">
|
|
||||||
|
|
||||||
<a href="https://americancloud.com/?ref=dokploy" target="_blank" style="display: inline-block; padding: 10px; border-radius: 10px;">
|
|
||||||
<img src=".github/sponsors/american-cloud.png" alt="AmericanCloud" height="70"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<!-- Elite Contributors 🥈 -->
|
<!-- Elite Contributors 🥈 -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Add Elite Contributors here -->
|
<!-- Add Elite Contributors here -->
|
||||||
|
|
||||||
### Supporting Members 🥉
|
### Elite Contributors 🥈
|
||||||
|
|
||||||
<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://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>
|
|
||||||
<a href="https://itsdb-center.com?ref=dokploy "><img src=".github/sponsors/its.png" width="65px" alt="Itsdb-center"/></a>
|
|
||||||
<a href="https://openalternative.co/?ref=dokploy "><img src=".github/sponsors/openalternative.png" width="65px" alt="Openalternative"/></a>
|
|
||||||
<a href="https://synexa.ai/?ref=dokploy"><img src=".github/sponsors/synexa.png" width="65px" alt="Synexa"/></a>
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="https://americancloud.com/?ref=dokploy"><img src=".github/sponsors/american-cloud.png" alt="AmericanCloud" width="300"/></a>
|
||||||
|
<a href="https://tolgee.io/?utm_source=github_dokploy&utm_medium=banner&utm_campaign=dokploy"><img src="https://dokploy.com/tolgee-logo.png" alt="Tolgee" width="100"/></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
### Supporting Members 🥉
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<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://synexa.ai/?ref=dokploy"><img src=".github/sponsors/synexa.png" width="65px" alt="Synexa"/></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
### Community Backers 🤝
|
### Community Backers 🤝
|
||||||
|
|
||||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
|
||||||
<a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
|
|
||||||
<a href="https://rivo.gg/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/126797452?s=200&v=4" width="60px" alt="Rivo.gg"/></a>
|
|
||||||
<a href="https://photoquest.wedding/?ref=dokploy"><img src="https://photoquest.wedding/favicon/android-chrome-512x512.png" width="60px" alt="Rivo.gg"/></a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
#### Organizations:
|
#### Organizations:
|
||||||
|
|
||||||
[](https://opencollective.com/dokploy)
|
[Sponsors on Open Collective](https://opencollective.com/dokploy)
|
||||||
|
|
||||||
#### Individuals:
|
#### Individuals:
|
||||||
|
|
||||||
@@ -139,15 +106,15 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
### Contributors 🤝
|
### Contributors 🤝
|
||||||
|
|
||||||
<a href="https://github.com/dokploy/dokploy/graphs/contributors">
|
<a href="https://github.com/dokploy/dokploy/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=dokploy/dokploy" />
|
<img src="https://contrib.rocks/image?repo=dokploy/dokploy" alt="Contributors" />
|
||||||
</a>
|
|
||||||
|
|
||||||
## Video Tutorial
|
|
||||||
|
|
||||||
<a href="https://youtu.be/mznYKPvhcfw">
|
|
||||||
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400" style="border-radius:20px;"/>
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## Contributing
|
## 📺 Video Tutorial
|
||||||
|
|
||||||
|
<a href="https://youtu.be/mznYKPvhcfw">
|
||||||
|
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
Check out the [Contributing Guide](CONTRIBUTING.md) for more information.
|
Check out the [Contributing Guide](CONTRIBUTING.md) for more information.
|
||||||
|
|||||||
@@ -9,25 +9,30 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"inngest": "3.40.1",
|
||||||
"@dokploy/server": "workspace:*",
|
"@dokploy/server": "workspace:*",
|
||||||
"@hono/node-server": "^1.12.1",
|
"@hono/node-server": "^1.14.3",
|
||||||
"@hono/zod-validator": "0.3.0",
|
"@hono/zod-validator": "0.3.0",
|
||||||
"@nerimity/mimiqueue": "1.2.3",
|
"@nerimity/mimiqueue": "1.2.3",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.4.5",
|
||||||
"hono": "^4.5.8",
|
"hono": "^4.7.10",
|
||||||
"pino": "9.4.0",
|
"pino": "9.4.0",
|
||||||
"pino-pretty": "11.2.2",
|
"pino-pretty": "11.2.2",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"redis": "4.7.0",
|
"redis": "4.7.0",
|
||||||
"zod": "^3.23.4"
|
"zod": "^3.25.32"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.11.17",
|
"@types/node": "^20.17.51",
|
||||||
"@types/react": "^18.2.37",
|
"@types/react": "^18.2.37",
|
||||||
"@types/react-dom": "^18.2.15",
|
"@types/react-dom": "^18.2.15",
|
||||||
"tsx": "^4.7.1",
|
"tsx": "^4.16.2",
|
||||||
"typescript": "^5.4.2"
|
"typescript": "^5.8.3"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.5.0"
|
"packageManager": "pnpm@9.12.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.16.0",
|
||||||
|
"pnpm": ">=9.12.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,21 +2,79 @@ import { serve } from "@hono/node-server";
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { Queue } from "@nerimity/mimiqueue";
|
import { Inngest } from "inngest";
|
||||||
import { createClient } from "redis";
|
import { serve as serveInngest } from "inngest/hono";
|
||||||
import { logger } from "./logger.js";
|
import { logger } from "./logger.js";
|
||||||
import { type DeployJob, deployJobSchema } from "./schema.js";
|
import { type DeployJob, deployJobSchema } from "./schema.js";
|
||||||
import { deploy } from "./utils.js";
|
import { deploy } from "./utils.js";
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
const redisClient = createClient({
|
|
||||||
url: process.env.REDIS_URL,
|
// Initialize Inngest client
|
||||||
|
export const inngest = new Inngest({
|
||||||
|
id: "dokploy-deployments",
|
||||||
|
name: "Dokploy Deployment Service",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const deploymentFunction = inngest.createFunction(
|
||||||
|
{
|
||||||
|
id: "deploy-application",
|
||||||
|
name: "Deploy Application",
|
||||||
|
concurrency: [
|
||||||
|
{
|
||||||
|
key: "event.data.serverId",
|
||||||
|
limit: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
retries: 0,
|
||||||
|
},
|
||||||
|
{ event: "deployment/requested" },
|
||||||
|
|
||||||
|
async ({ event, step }) => {
|
||||||
|
const jobData = event.data as DeployJob;
|
||||||
|
|
||||||
|
return await step.run("execute-deployment", async () => {
|
||||||
|
logger.info("Deploying started");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await deploy(jobData);
|
||||||
|
logger.info("Deployment finished", result);
|
||||||
|
|
||||||
|
// Send success event
|
||||||
|
await inngest.send({
|
||||||
|
name: "deployment/completed",
|
||||||
|
data: {
|
||||||
|
...jobData,
|
||||||
|
result,
|
||||||
|
status: "success",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Deployment failed", { jobData, error });
|
||||||
|
|
||||||
|
// Send failure event
|
||||||
|
await inngest.send({
|
||||||
|
name: "deployment/failed",
|
||||||
|
data: {
|
||||||
|
...jobData,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
status: "failed",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
app.use(async (c, next) => {
|
app.use(async (c, next) => {
|
||||||
if (c.req.path === "/health") {
|
if (c.req.path === "/health" || c.req.path === "/api/inngest") {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
const authHeader = c.req.header("X-API-Key");
|
const authHeader = c.req.header("X-API-Key");
|
||||||
|
|
||||||
if (process.env.API_KEY !== authHeader) {
|
if (process.env.API_KEY !== authHeader) {
|
||||||
@@ -26,36 +84,55 @@ app.use(async (c, next) => {
|
|||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/deploy", zValidator("json", deployJobSchema), (c) => {
|
app.post("/deploy", zValidator("json", deployJobSchema), async (c) => {
|
||||||
const data = c.req.valid("json");
|
const data = c.req.valid("json");
|
||||||
queue.add(data, { groupName: data.serverId });
|
logger.info("Received deployment request", data);
|
||||||
return c.json(
|
|
||||||
{
|
try {
|
||||||
message: "Deployment Added",
|
// Send event to Inngest instead of adding to Redis queue
|
||||||
},
|
await inngest.send({
|
||||||
200,
|
name: "deployment/requested",
|
||||||
);
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info("Deployment event sent to Inngest", {
|
||||||
|
serverId: data.serverId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
message: "Deployment Added to Inngest Queue",
|
||||||
|
serverId: data.serverId,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("error", error);
|
||||||
|
logger.error("Failed to send deployment event", error);
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
message: "Failed to queue deployment",
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
},
|
||||||
|
500,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/health", async (c) => {
|
app.get("/health", async (c) => {
|
||||||
return c.json({ status: "ok" });
|
return c.json({ status: "ok" });
|
||||||
});
|
});
|
||||||
|
|
||||||
const queue = new Queue({
|
// Serve Inngest functions endpoint
|
||||||
name: "deployments",
|
app.on(
|
||||||
process: async (job: DeployJob) => {
|
["GET", "POST", "PUT"],
|
||||||
logger.info("Deploying job", job);
|
"/api/inngest",
|
||||||
return await deploy(job);
|
serveInngest({
|
||||||
},
|
client: inngest,
|
||||||
redisClient,
|
functions: [deploymentFunction],
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
(async () => {
|
|
||||||
await redisClient.connect();
|
|
||||||
await redisClient.flushAll();
|
|
||||||
logger.info("Redis Cleaned");
|
|
||||||
})();
|
|
||||||
|
|
||||||
const port = Number.parseInt(process.env.PORT || "3000");
|
const port = Number.parseInt(process.env.PORT || "3000");
|
||||||
logger.info("Starting Deployments Server ✅", port);
|
logger.info("Starting Deployments Server with Inngest ✅", port);
|
||||||
serve({ fetch: app.fetch, port });
|
serve({ fetch: app.fetch, port });
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const deploy = async (job: DeployJob) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (e) {
|
||||||
if (job.applicationType === "application") {
|
if (job.applicationType === "application") {
|
||||||
await updateApplicationStatus(job.applicationId, "error");
|
await updateApplicationStatus(job.applicationId, "error");
|
||||||
} else if (job.applicationType === "compose") {
|
} else if (job.applicationType === "compose") {
|
||||||
@@ -76,6 +76,8 @@ export const deploy = async (job: DeployJob) => {
|
|||||||
previewStatus: "error",
|
previewStatus: "error",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
# License
|
|
||||||
|
|
||||||
## Core License (Apache License 2.0)
|
|
||||||
|
|
||||||
Copyright 2024 Mauricio Siu.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and limitations under the License.
|
|
||||||
|
|
||||||
## Additional Terms for Specific Features
|
|
||||||
|
|
||||||
The following additional terms apply to the multi-node support, Docker Compose file, Preview Deployments and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
|
||||||
|
|
||||||
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Preview Deployments and Multi Server, will always be free to use in the self-hosted version.
|
|
||||||
- **Restriction on Resale**: The multi-node support, Docker Compose file support, Preview Deployments and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
|
||||||
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support, Preview Deployments and Multi Server features must be distributed freely and cannot be sold or offered as a service.
|
|
||||||
|
|
||||||
For further inquiries or permissions, please contact us directly.
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { addSuffixToAllProperties } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
|
import { addSuffixToAllProperties } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToConfigsRoot } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
|
import { addSuffixToConfigsRoot, generateRandomHash } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToConfigsInServices } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
|
import {
|
||||||
|
addSuffixToConfigsInServices,
|
||||||
|
generateRandomHash,
|
||||||
|
} from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToAllConfigs } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
|
import { addSuffixToAllConfigs, generateRandomHash } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ describe("createDomainLabels", () => {
|
|||||||
path: "/",
|
path: "/",
|
||||||
createdAt: "",
|
createdAt: "",
|
||||||
previewDeploymentId: "",
|
previewDeploymentId: "",
|
||||||
|
internalPath: "/",
|
||||||
|
stripPath: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
it("should create basic labels for web entrypoint", async () => {
|
it("should create basic labels for web entrypoint", async () => {
|
||||||
@@ -106,4 +108,136 @@ describe("createDomainLabels", () => {
|
|||||||
"traefik.http.services.test-app-1-web.loadbalancer.server.port=3000",
|
"traefik.http.services.test-app-1-web.loadbalancer.server.port=3000",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should add stripPath middleware when stripPath is enabled", async () => {
|
||||||
|
const stripPathDomain = {
|
||||||
|
...baseDomain,
|
||||||
|
path: "/api",
|
||||||
|
stripPath: true,
|
||||||
|
};
|
||||||
|
const labels = await createDomainLabels(appName, stripPathDomain, "web");
|
||||||
|
|
||||||
|
expect(labels).toContain(
|
||||||
|
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
|
||||||
|
);
|
||||||
|
expect(labels).toContain(
|
||||||
|
"traefik.http.routers.test-app-1-web.middlewares=stripprefix-test-app-1",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add internalPath middleware when internalPath is set", async () => {
|
||||||
|
const internalPathDomain = {
|
||||||
|
...baseDomain,
|
||||||
|
internalPath: "/hello",
|
||||||
|
};
|
||||||
|
const webLabels = await createDomainLabels(
|
||||||
|
appName,
|
||||||
|
internalPathDomain,
|
||||||
|
"web",
|
||||||
|
);
|
||||||
|
const websecureLabels = await createDomainLabels(
|
||||||
|
appName,
|
||||||
|
internalPathDomain,
|
||||||
|
"websecure",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Middleware definition should only appear in web entrypoint
|
||||||
|
expect(webLabels).toContain(
|
||||||
|
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
|
||||||
|
);
|
||||||
|
expect(websecureLabels).not.toContain(
|
||||||
|
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Both routers should reference the middleware
|
||||||
|
expect(webLabels).toContain(
|
||||||
|
"traefik.http.routers.test-app-1-web.middlewares=addprefix-test-app-1",
|
||||||
|
);
|
||||||
|
expect(websecureLabels).toContain(
|
||||||
|
"traefik.http.routers.test-app-1-websecure.middlewares=addprefix-test-app-1",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should combine HTTPS redirect with internalPath middleware in correct order", async () => {
|
||||||
|
const combinedDomain = {
|
||||||
|
...baseDomain,
|
||||||
|
https: true,
|
||||||
|
internalPath: "/hello",
|
||||||
|
};
|
||||||
|
const webLabels = await createDomainLabels(appName, combinedDomain, "web");
|
||||||
|
const websecureLabels = await createDomainLabels(
|
||||||
|
appName,
|
||||||
|
combinedDomain,
|
||||||
|
"websecure",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Web entrypoint should have both middlewares with redirect first
|
||||||
|
expect(webLabels).toContain(
|
||||||
|
"traefik.http.routers.test-app-1-web.middlewares=redirect-to-https@file,addprefix-test-app-1",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Websecure should only have the addprefix middleware
|
||||||
|
expect(websecureLabels).toContain(
|
||||||
|
"traefik.http.routers.test-app-1-websecure.middlewares=addprefix-test-app-1",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Middleware definition should only appear once (in web)
|
||||||
|
expect(webLabels).toContain(
|
||||||
|
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
|
||||||
|
);
|
||||||
|
expect(websecureLabels).not.toContain(
|
||||||
|
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should combine all middlewares in correct order", async () => {
|
||||||
|
const fullDomain = {
|
||||||
|
...baseDomain,
|
||||||
|
https: true,
|
||||||
|
path: "/api",
|
||||||
|
stripPath: true,
|
||||||
|
internalPath: "/hello",
|
||||||
|
};
|
||||||
|
const webLabels = await createDomainLabels(appName, fullDomain, "web");
|
||||||
|
|
||||||
|
// Should have all middleware definitions (only in web)
|
||||||
|
expect(webLabels).toContain(
|
||||||
|
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
|
||||||
|
);
|
||||||
|
expect(webLabels).toContain(
|
||||||
|
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should have middlewares in correct order: redirect, stripprefix, addprefix
|
||||||
|
expect(webLabels).toContain(
|
||||||
|
"traefik.http.routers.test-app-1-web.middlewares=redirect-to-https@file,stripprefix-test-app-1,addprefix-test-app-1",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not add middleware definitions for websecure entrypoint", async () => {
|
||||||
|
const internalPathDomain = {
|
||||||
|
...baseDomain,
|
||||||
|
path: "/api",
|
||||||
|
stripPath: true,
|
||||||
|
internalPath: "/hello",
|
||||||
|
};
|
||||||
|
const websecureLabels = await createDomainLabels(
|
||||||
|
appName,
|
||||||
|
internalPathDomain,
|
||||||
|
"websecure",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should not contain any middleware definitions
|
||||||
|
expect(websecureLabels).not.toContain(
|
||||||
|
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
|
||||||
|
);
|
||||||
|
expect(websecureLabels).not.toContain(
|
||||||
|
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
|
||||||
|
);
|
||||||
|
|
||||||
|
// But should reference the middlewares
|
||||||
|
expect(websecureLabels).toContain(
|
||||||
|
"traefik.http.routers.test-app-1-websecure.middlewares=stripprefix-test-app-1,addprefix-test-app-1",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToNetworksRoot } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
|
import { addSuffixToNetworksRoot, generateRandomHash } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToServiceNetworks } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
|
import {
|
||||||
|
addSuffixToServiceNetworks,
|
||||||
|
generateRandomHash,
|
||||||
|
} from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import {
|
import {
|
||||||
addSuffixToAllNetworks,
|
addSuffixToAllNetworks,
|
||||||
|
addSuffixToNetworksRoot,
|
||||||
addSuffixToServiceNetworks,
|
addSuffixToServiceNetworks,
|
||||||
|
generateRandomHash,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { addSuffixToNetworksRoot } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToSecretsRoot } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
|
import { addSuffixToSecretsRoot, generateRandomHash } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToSecretsInServices } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
|
import {
|
||||||
|
addSuffixToSecretsInServices,
|
||||||
|
generateRandomHash,
|
||||||
|
} from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { addSuffixToAllSecrets } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
|
import { addSuffixToAllSecrets } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
|
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
|
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
|
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
|
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
|
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import {
|
import {
|
||||||
addSuffixToAllServiceNames,
|
addSuffixToAllServiceNames,
|
||||||
addSuffixToServiceNames,
|
addSuffixToServiceNames,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
|
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToAllVolumes, addSuffixToVolumesRoot } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
|
import {
|
||||||
|
addSuffixToAllVolumes,
|
||||||
|
addSuffixToVolumesRoot,
|
||||||
|
generateRandomHash,
|
||||||
|
} from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToVolumesRoot } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
|
import { addSuffixToVolumesRoot, generateRandomHash } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToVolumesInServices } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
|
import {
|
||||||
|
addSuffixToVolumesInServices,
|
||||||
|
generateRandomHash,
|
||||||
|
} from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { addSuffixToAllVolumes } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
|
import { addSuffixToAllVolumes } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]";
|
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]";
|
||||||
|
|
||||||
describe("GitHub Webhook Skip CI", () => {
|
describe("GitHub Webhook Skip CI", () => {
|
||||||
const mockGithubHeaders = {
|
const mockGithubHeaders = {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { paths } from "@dokploy/server/constants";
|
|
||||||
const { APPLICATIONS_PATH } = paths();
|
|
||||||
import type { ApplicationNested } from "@dokploy/server";
|
import type { ApplicationNested } from "@dokploy/server";
|
||||||
import { unzipDrop } from "@dokploy/server";
|
import { unzipDrop } from "@dokploy/server";
|
||||||
|
import { paths } from "@dokploy/server/constants";
|
||||||
import AdmZip from "adm-zip";
|
import AdmZip from "adm-zip";
|
||||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const { APPLICATIONS_PATH } = paths();
|
||||||
vi.mock("@dokploy/server/constants", async (importOriginal) => {
|
vi.mock("@dokploy/server/constants", async (importOriginal) => {
|
||||||
const actual = await importOriginal();
|
const actual = await importOriginal();
|
||||||
return {
|
return {
|
||||||
@@ -25,10 +25,13 @@ if (typeof window === "undefined") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const baseApp: ApplicationNested = {
|
const baseApp: ApplicationNested = {
|
||||||
|
railpackVersion: "0.2.2",
|
||||||
applicationId: "",
|
applicationId: "",
|
||||||
|
previewLabels: [],
|
||||||
herokuVersion: "",
|
herokuVersion: "",
|
||||||
giteaBranch: "",
|
giteaBranch: "",
|
||||||
giteaBuildPath: "",
|
giteaBuildPath: "",
|
||||||
|
previewRequireCollaboratorPermissions: false,
|
||||||
giteaId: "",
|
giteaId: "",
|
||||||
giteaOwner: "",
|
giteaOwner: "",
|
||||||
giteaRepository: "",
|
giteaRepository: "",
|
||||||
@@ -141,7 +144,7 @@ describe("unzipDrop using real zip files", () => {
|
|||||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||||
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
|
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
|
||||||
console.log(`Output Path: ${outputPath}`);
|
console.log(`Output Path: ${outputPath}`);
|
||||||
const zipBuffer = zip.toBuffer();
|
const zipBuffer = zip.toBuffer() as Buffer<ArrayBuffer>;
|
||||||
const file = new File([zipBuffer], "single.zip");
|
const file = new File([zipBuffer], "single.zip");
|
||||||
await unzipDrop(file, baseApp);
|
await unzipDrop(file, baseApp);
|
||||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
74
apps/dokploy/__test__/env/shared.test.ts
vendored
74
apps/dokploy/__test__/env/shared.test.ts
vendored
@@ -177,3 +177,77 @@ COMPLEX_VAR="'Prefix \"DoubleQuoted\" and \${{project.APP_NAME}}'"
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("prepareEnvironmentVariables (self references)", () => {
|
||||||
|
it("resolves self references correctly", () => {
|
||||||
|
const serviceEnv = `
|
||||||
|
ENVIRONMENT=staging
|
||||||
|
DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db
|
||||||
|
SELF_REF=\${{ENVIRONMENT}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, "");
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"ENVIRONMENT=staging",
|
||||||
|
"DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db",
|
||||||
|
"SELF_REF=staging",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws on undefined self references", () => {
|
||||||
|
const serviceEnv = `
|
||||||
|
MISSING_VAR=\${{UNDEFINED_VAR}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
expect(() => prepareEnvironmentVariables(serviceEnv, "")).toThrow(
|
||||||
|
"Invalid service environment variable: UNDEFINED_VAR",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows overriding and still resolving from self", () => {
|
||||||
|
const serviceEnv = `
|
||||||
|
ENVIRONMENT=production
|
||||||
|
OVERRIDE_ENV=\${{ENVIRONMENT}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, "");
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"ENVIRONMENT=production",
|
||||||
|
"OVERRIDE_ENV=production",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves multiple self references inside one value", () => {
|
||||||
|
const serviceEnv = `
|
||||||
|
ENVIRONMENT=staging
|
||||||
|
APP_NAME=MyApp
|
||||||
|
COMPLEX=\${{APP_NAME}}-\${{ENVIRONMENT}}-\${{APP_NAME}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, "");
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"ENVIRONMENT=staging",
|
||||||
|
"APP_NAME=MyApp",
|
||||||
|
"COMPLEX=MyApp-staging-MyApp",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles quotes with self references", () => {
|
||||||
|
const serviceEnv = `
|
||||||
|
ENVIRONMENT=production
|
||||||
|
QUOTED="'\${{ENVIRONMENT}}'"
|
||||||
|
MIXED="\"Double \${{ENVIRONMENT}}\""
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, "");
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"ENVIRONMENT=production",
|
||||||
|
"QUOTED='production'",
|
||||||
|
'MIXED="Double production"',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { parseRawConfig, processLogs } from "@dokploy/server";
|
import { parseRawConfig, processLogs } from "@dokploy/server";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`;
|
const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`;
|
||||||
|
|
||||||
describe("processLogs", () => {
|
describe("processLogs", () => {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type { Domain } from "@dokploy/server";
|
import type { ApplicationNested, Domain, Redirect } from "@dokploy/server";
|
||||||
import type { Redirect } from "@dokploy/server";
|
|
||||||
import type { ApplicationNested } from "@dokploy/server";
|
|
||||||
import { createRouterConfig } from "@dokploy/server";
|
import { createRouterConfig } from "@dokploy/server";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
const baseApp: ApplicationNested = {
|
const baseApp: ApplicationNested = {
|
||||||
|
railpackVersion: "0.2.2",
|
||||||
rollbackActive: false,
|
rollbackActive: false,
|
||||||
applicationId: "",
|
applicationId: "",
|
||||||
|
previewLabels: [],
|
||||||
herokuVersion: "",
|
herokuVersion: "",
|
||||||
giteaRepository: "",
|
giteaRepository: "",
|
||||||
giteaOwner: "",
|
giteaOwner: "",
|
||||||
@@ -18,6 +18,7 @@ const baseApp: ApplicationNested = {
|
|||||||
appName: "",
|
appName: "",
|
||||||
autoDeploy: true,
|
autoDeploy: true,
|
||||||
enableSubmodules: false,
|
enableSubmodules: false,
|
||||||
|
previewRequireCollaboratorPermissions: false,
|
||||||
serverId: "",
|
serverId: "",
|
||||||
branch: null,
|
branch: null,
|
||||||
dockerBuildStage: "",
|
dockerBuildStage: "",
|
||||||
@@ -119,6 +120,8 @@ const baseDomain: Domain = {
|
|||||||
domainType: "application",
|
domainType: "application",
|
||||||
uniqueConfigKey: 1,
|
uniqueConfigKey: 1,
|
||||||
previewDeploymentId: "",
|
previewDeploymentId: "",
|
||||||
|
internalPath: "/",
|
||||||
|
stripPath: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseRedirect: Redirect = {
|
const baseRedirect: Redirect = {
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { HelpCircle, Settings } from "lucide-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -26,12 +32,6 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { HelpCircle, Settings } from "lucide-react";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const HealthCheckSwarmSchema = z
|
const HealthCheckSwarmSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -130,7 +130,7 @@ const createStringToJSONSchema = (schema: z.ZodTypeAny) => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return JSON.parse(str);
|
return JSON.parse(str);
|
||||||
} catch (_e) {
|
} catch {
|
||||||
ctx.addIssue({ code: "custom", message: "Invalid JSON format" });
|
ctx.addIssue({ code: "custom", message: "Invalid JSON format" });
|
||||||
return z.NEVER;
|
return z.NEVER;
|
||||||
}
|
}
|
||||||
@@ -181,21 +181,38 @@ const addSwarmSettings = z.object({
|
|||||||
type AddSwarmSettings = z.infer<typeof addSwarmSettings>;
|
type AddSwarmSettings = z.infer<typeof addSwarmSettings>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
id: string;
|
||||||
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddSwarmSettings = ({ applicationId }: Props) => {
|
export const AddSwarmSettings = ({ id, type }: Props) => {
|
||||||
const { data, refetch } = api.application.one.useQuery(
|
const queryMap = {
|
||||||
{
|
postgres: () =>
|
||||||
applicationId,
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
},
|
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||||
{
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
enabled: !!applicationId,
|
mariadb: () =>
|
||||||
},
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
);
|
application: () =>
|
||||||
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
|
};
|
||||||
|
const { data, refetch } = queryMap[type]
|
||||||
|
? queryMap[type]()
|
||||||
|
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||||
|
|
||||||
const { mutateAsync, isError, error, isLoading } =
|
const mutationMap = {
|
||||||
api.application.update.useMutation();
|
postgres: () => api.postgres.update.useMutation(),
|
||||||
|
redis: () => api.redis.update.useMutation(),
|
||||||
|
mysql: () => api.mysql.update.useMutation(),
|
||||||
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
|
application: () => api.application.update.useMutation(),
|
||||||
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { mutateAsync, isError, error, isLoading } = mutationMap[type]
|
||||||
|
? mutationMap[type]()
|
||||||
|
: api.mongo.update.useMutation();
|
||||||
|
|
||||||
const form = useForm<AddSwarmSettings>({
|
const form = useForm<AddSwarmSettings>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -244,7 +261,12 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
|||||||
|
|
||||||
const onSubmit = async (data: AddSwarmSettings) => {
|
const onSubmit = async (data: AddSwarmSettings) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
applicationId,
|
applicationId: id || "",
|
||||||
|
postgresId: id || "",
|
||||||
|
redisId: id || "",
|
||||||
|
mysqlId: id || "",
|
||||||
|
mariadbId: id || "",
|
||||||
|
mongoId: id || "",
|
||||||
healthCheckSwarm: data.healthCheckSwarm,
|
healthCheckSwarm: data.healthCheckSwarm,
|
||||||
restartPolicySwarm: data.restartPolicySwarm,
|
restartPolicySwarm: data.restartPolicySwarm,
|
||||||
placementSwarm: data.placementSwarm,
|
placementSwarm: data.placementSwarm,
|
||||||
@@ -270,18 +292,18 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
|||||||
Swarm Settings
|
Swarm Settings
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-5xl p-0">
|
<DialogContent className="sm:max-w-5xl">
|
||||||
<DialogHeader className="p-6">
|
<DialogHeader>
|
||||||
<DialogTitle>Swarm Settings</DialogTitle>
|
<DialogTitle>Swarm Settings</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Update certain settings using a json object.
|
Update certain settings using a json object.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="px-4">
|
<div>
|
||||||
<AlertBlock type="info">
|
<AlertBlock type="info">
|
||||||
Changing settings such as placements may cause the logs/monitoring
|
Changing settings such as placements may cause the logs/monitoring,
|
||||||
to be unavailable.
|
backups and other features to be unavailable.
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -289,13 +311,13 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
|||||||
<form
|
<form
|
||||||
id="hook-form-add-permissions"
|
id="hook-form-add-permissions"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid grid-cols-1 md:grid-cols-2 w-full gap-4 relative"
|
className="grid grid-cols-1 md:grid-cols-2 w-full gap-4 relative mt-4"
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="healthCheckSwarm"
|
name="healthCheckSwarm"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
|
<FormItem className="relative ">
|
||||||
<FormLabel>Health Check</FormLabel>
|
<FormLabel>Health Check</FormLabel>
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -351,7 +373,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="restartPolicySwarm"
|
name="restartPolicySwarm"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
|
<FormItem className="relative ">
|
||||||
<FormLabel>Restart Policy</FormLabel>
|
<FormLabel>Restart Policy</FormLabel>
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -405,7 +427,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="placementSwarm"
|
name="placementSwarm"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
|
<FormItem className="relative ">
|
||||||
<FormLabel>Placement</FormLabel>
|
<FormLabel>Placement</FormLabel>
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -471,7 +493,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="updateConfigSwarm"
|
name="updateConfigSwarm"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
|
<FormItem className="relative ">
|
||||||
<FormLabel>Update Config</FormLabel>
|
<FormLabel>Update Config</FormLabel>
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -529,7 +551,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="rollbackConfigSwarm"
|
name="rollbackConfigSwarm"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
|
<FormItem className="relative ">
|
||||||
<FormLabel>Rollback Config</FormLabel>
|
<FormLabel>Rollback Config</FormLabel>
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -587,7 +609,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="modeSwarm"
|
name="modeSwarm"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
|
<FormItem className="relative ">
|
||||||
<FormLabel>Mode</FormLabel>
|
<FormLabel>Mode</FormLabel>
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -650,7 +672,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="networkSwarm"
|
name="networkSwarm"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
|
<FormItem className="relative ">
|
||||||
<FormLabel>Network</FormLabel>
|
<FormLabel>Network</FormLabel>
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -709,7 +731,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="labelsSwarm"
|
name="labelsSwarm"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
|
<FormItem className="relative ">
|
||||||
<FormLabel>Labels</FormLabel>
|
<FormLabel>Labels</FormLabel>
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -753,7 +775,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2 m-0 sticky bottom-0 right-0 bg-muted border p-2 ">
|
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2 m-0 sticky bottom-0 right-0 bg-muted border">
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
form="hook-form-add-permissions"
|
form="hook-form-add-permissions"
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Server } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -26,43 +33,57 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} 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 { Server } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { AddSwarmSettings } from "./modify-swarm-settings";
|
import { AddSwarmSettings } from "./modify-swarm-settings";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
id: string;
|
||||||
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddRedirectchema = z.object({
|
const AddRedirectchema = z.object({
|
||||||
replicas: z.number().min(1, "Replicas must be at least 1"),
|
replicas: z.number().min(1, "Replicas must be at least 1"),
|
||||||
registryId: z.string(),
|
registryId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddCommand = z.infer<typeof AddRedirectchema>;
|
type AddCommand = z.infer<typeof AddRedirectchema>;
|
||||||
|
|
||||||
export const ShowClusterSettings = ({ applicationId }: Props) => {
|
export const ShowClusterSettings = ({ id, type }: Props) => {
|
||||||
const { data } = api.application.one.useQuery(
|
const queryMap = {
|
||||||
{
|
postgres: () =>
|
||||||
applicationId,
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
},
|
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||||
{ enabled: !!applicationId },
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
);
|
mariadb: () =>
|
||||||
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
|
application: () =>
|
||||||
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
|
};
|
||||||
|
const { data, refetch } = queryMap[type]
|
||||||
|
? queryMap[type]()
|
||||||
|
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||||
const { data: registries } = api.registry.all.useQuery();
|
const { data: registries } = api.registry.all.useQuery();
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const mutationMap = {
|
||||||
|
postgres: () => api.postgres.update.useMutation(),
|
||||||
|
redis: () => api.redis.update.useMutation(),
|
||||||
|
mysql: () => api.mysql.update.useMutation(),
|
||||||
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
|
application: () => api.application.update.useMutation(),
|
||||||
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
|
};
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = api.application.update.useMutation();
|
const { mutateAsync, isLoading } = mutationMap[type]
|
||||||
|
? mutationMap[type]()
|
||||||
|
: api.mongo.update.useMutation();
|
||||||
|
|
||||||
const form = useForm<AddCommand>({
|
const form = useForm<AddCommand>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
registryId: data?.registryId || "",
|
...(type === "application" && data && "registryId" in data
|
||||||
|
? {
|
||||||
|
registryId: data?.registryId || "",
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
replicas: data?.replicas || 1,
|
replicas: data?.replicas || 1,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(AddRedirectchema),
|
resolver: zodResolver(AddRedirectchema),
|
||||||
@@ -71,7 +92,11 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.command) {
|
if (data?.command) {
|
||||||
form.reset({
|
form.reset({
|
||||||
registryId: data?.registryId || "",
|
...(type === "application" && data && "registryId" in data
|
||||||
|
? {
|
||||||
|
registryId: data?.registryId || "",
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
replicas: data?.replicas || 1,
|
replicas: data?.replicas || 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -79,18 +104,25 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
|
|||||||
|
|
||||||
const onSubmit = async (data: AddCommand) => {
|
const onSubmit = async (data: AddCommand) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
applicationId,
|
applicationId: id || "",
|
||||||
registryId:
|
postgresId: id || "",
|
||||||
data?.registryId === "none" || !data?.registryId
|
redisId: id || "",
|
||||||
? null
|
mysqlId: id || "",
|
||||||
: data?.registryId,
|
mariadbId: id || "",
|
||||||
|
mongoId: id || "",
|
||||||
|
...(type === "application"
|
||||||
|
? {
|
||||||
|
registryId:
|
||||||
|
data?.registryId === "none" || !data?.registryId
|
||||||
|
? null
|
||||||
|
: data?.registryId,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
replicas: data?.replicas,
|
replicas: data?.replicas,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Command Updated");
|
toast.success("Command Updated");
|
||||||
await utils.application.one.invalidate({
|
await refetch();
|
||||||
applicationId,
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error updating the command");
|
toast.error("Error updating the command");
|
||||||
@@ -103,10 +135,10 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
|
|||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-xl">Cluster Settings</CardTitle>
|
<CardTitle className="text-xl">Cluster Settings</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Add the registry and the replicas of the application
|
Modify swarm settings for the service.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<AddSwarmSettings applicationId={applicationId} />
|
<AddSwarmSettings id={id} type={type} />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
<AlertBlock type="info">
|
<AlertBlock type="info">
|
||||||
@@ -144,58 +176,62 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{registries && registries?.length === 0 ? (
|
{type === "application" && (
|
||||||
<div className="pt-10">
|
|
||||||
<div className="flex flex-col items-center gap-3">
|
|
||||||
<Server className="size-8 text-muted-foreground" />
|
|
||||||
<span className="text-base text-muted-foreground">
|
|
||||||
To use a cluster feature, you need to configure at least a
|
|
||||||
registry first. Please, go to{" "}
|
|
||||||
<Link
|
|
||||||
href="/dashboard/settings/cluster"
|
|
||||||
className="text-foreground"
|
|
||||||
>
|
|
||||||
Settings
|
|
||||||
</Link>{" "}
|
|
||||||
to do so.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
<FormField
|
{registries && registries?.length === 0 ? (
|
||||||
control={form.control}
|
<div className="pt-10">
|
||||||
name="registryId"
|
<div className="flex flex-col items-center gap-3">
|
||||||
render={({ field }) => (
|
<Server className="size-8 text-muted-foreground" />
|
||||||
<FormItem>
|
<span className="text-base text-muted-foreground">
|
||||||
<FormLabel>Select a registry</FormLabel>
|
To use a cluster feature, you need to configure at least
|
||||||
<Select
|
a registry first. Please, go to{" "}
|
||||||
onValueChange={field.onChange}
|
<Link
|
||||||
defaultValue={field.value}
|
href="/dashboard/settings/cluster"
|
||||||
>
|
className="text-foreground"
|
||||||
<SelectTrigger>
|
>
|
||||||
<SelectValue placeholder="Select a registry" />
|
Settings
|
||||||
</SelectTrigger>
|
</Link>{" "}
|
||||||
<SelectContent>
|
to do so.
|
||||||
<SelectGroup>
|
</span>
|
||||||
{registries?.map((registry) => (
|
</div>
|
||||||
<SelectItem
|
</div>
|
||||||
key={registry.registryId}
|
) : (
|
||||||
value={registry.registryId}
|
<>
|
||||||
>
|
<FormField
|
||||||
{registry.registryName}
|
control={form.control}
|
||||||
</SelectItem>
|
name="registryId"
|
||||||
))}
|
render={({ field }) => (
|
||||||
<SelectItem value={"none"}>None</SelectItem>
|
<FormItem>
|
||||||
<SelectLabel>
|
<FormLabel>Select a registry</FormLabel>
|
||||||
Registries ({registries?.length})
|
<Select
|
||||||
</SelectLabel>
|
onValueChange={field.onChange}
|
||||||
</SelectGroup>
|
defaultValue={field.value}
|
||||||
</SelectContent>
|
>
|
||||||
</Select>
|
<SelectTrigger>
|
||||||
</FormItem>
|
<SelectValue placeholder="Select a registry" />
|
||||||
)}
|
</SelectTrigger>
|
||||||
/>
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{registries?.map((registry) => (
|
||||||
|
<SelectItem
|
||||||
|
key={registry.registryId}
|
||||||
|
value={registry.registryId}
|
||||||
|
>
|
||||||
|
{registry.registryName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectItem value={"none"}>None</SelectItem>
|
||||||
|
<SelectLabel>
|
||||||
|
Registries ({registries?.length})
|
||||||
|
</SelectLabel>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -16,11 +21,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Code2, Globe2, HardDrive } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -27,12 +33,6 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
|||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { Code2, Globe2, HardDrive } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const ImportSchema = z.object({
|
const ImportSchema = z.object({
|
||||||
base64: z.string(),
|
base64: z.string(),
|
||||||
@@ -107,7 +107,7 @@ export const ShowImport = ({ composeId }: Props) => {
|
|||||||
composeId,
|
composeId,
|
||||||
});
|
});
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
} catch (_error) {
|
} catch {
|
||||||
toast.error("Error importing template");
|
toast.error("Error importing template");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -126,7 +126,7 @@ export const ShowImport = ({ composeId }: Props) => {
|
|||||||
});
|
});
|
||||||
setTemplateInfo(result);
|
setTemplateInfo(result);
|
||||||
setShowModal(true);
|
setShowModal(true);
|
||||||
} catch (_error) {
|
} catch {
|
||||||
toast.error("Error processing template");
|
toast.error("Error processing template");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -185,7 +185,7 @@ export const ShowImport = ({ composeId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Dialog open={showModal} onOpenChange={setShowModal}>
|
<Dialog open={showModal} onOpenChange={setShowModal}>
|
||||||
<DialogContent className="max-h-[80vh] max-w-[50vw] overflow-y-auto">
|
<DialogContent className="max-w-[50vw]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-2xl font-bold">
|
<DialogTitle className="text-2xl font-bold">
|
||||||
Template Information
|
Template Information
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm, useWatch } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -26,15 +32,12 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} 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 { PenBoxIcon, PlusIcon } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const AddPortSchema = z.object({
|
const AddPortSchema = z.object({
|
||||||
publishedPort: z.number().int().min(1).max(65535),
|
publishedPort: z.number().int().min(1).max(65535),
|
||||||
|
publishMode: z.enum(["ingress", "host"], {
|
||||||
|
required_error: "Publish mode is required",
|
||||||
|
}),
|
||||||
targetPort: z.number().int().min(1).max(65535),
|
targetPort: z.number().int().min(1).max(65535),
|
||||||
protocol: z.enum(["tcp", "udp"], {
|
protocol: z.enum(["tcp", "udp"], {
|
||||||
required_error: "Protocol is required",
|
required_error: "Protocol is required",
|
||||||
@@ -77,9 +80,15 @@ export const HandlePorts = ({
|
|||||||
resolver: zodResolver(AddPortSchema),
|
resolver: zodResolver(AddPortSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const publishMode = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "publishMode",
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
publishedPort: data?.publishedPort ?? 0,
|
publishedPort: data?.publishedPort ?? 0,
|
||||||
|
publishMode: data?.publishMode ?? "ingress",
|
||||||
targetPort: data?.targetPort ?? 0,
|
targetPort: data?.targetPort ?? 0,
|
||||||
protocol: data?.protocol ?? "tcp",
|
protocol: data?.protocol ?? "tcp",
|
||||||
});
|
});
|
||||||
@@ -120,7 +129,7 @@ export const HandlePorts = ({
|
|||||||
<Button>{children}</Button>
|
<Button>{children}</Button>
|
||||||
)}
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Ports</DialogTitle>
|
<DialogTitle>Ports</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -165,6 +174,32 @@ export const HandlePorts = ({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="publishMode"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem className="md:col-span-2">
|
||||||
|
<FormLabel>Published Port Mode</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
value={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a publish mode for the port" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={"ingress"}>Ingress</SelectItem>
|
||||||
|
<SelectItem value={"host"}>Host</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="targetPort"
|
name="targetPort"
|
||||||
@@ -223,6 +258,16 @@ export const HandlePorts = ({
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{publishMode === "host" && (
|
||||||
|
<AlertBlock type="warning" className="mt-4">
|
||||||
|
<strong>Host Mode Limitation:</strong> When using Host publish
|
||||||
|
mode, Docker Swarm has limitations that prevent proper container
|
||||||
|
updates during deployments. Old containers may not be replaced
|
||||||
|
automatically. Consider using Ingress mode instead, or be prepared
|
||||||
|
to manually stop/start the application after deployments.
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Rss, Trash2 } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -9,9 +11,8 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Rss, Trash2 } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { HandlePorts } from "./handle-ports";
|
import { HandlePorts } from "./handle-ports";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
@@ -60,7 +61,7 @@ export const ShowPorts = ({ applicationId }: Props) => {
|
|||||||
{data?.ports.map((port) => (
|
{data?.ports.map((port) => (
|
||||||
<div key={port.portId}>
|
<div key={port.portId}>
|
||||||
<div className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4">
|
<div className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 flex-col gap-4 sm:gap-8">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 flex-col gap-4 sm:gap-8">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="font-medium">Published Port</span>
|
<span className="font-medium">Published Port</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
@@ -68,7 +69,13 @@ export const ShowPorts = ({ applicationId }: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="font-medium"> Target Port</span>
|
<span className="font-medium">Published Port Mode</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{port?.publishMode?.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="font-medium">Target Port</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{port.targetPort}
|
{port.targetPort}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -30,12 +36,6 @@ import {
|
|||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const AddRedirectchema = z.object({
|
const AddRedirectchema = z.object({
|
||||||
regex: z.string().min(1, "Regex required"),
|
regex: z.string().min(1, "Regex required"),
|
||||||
@@ -179,7 +179,7 @@ export const HandleRedirect = ({
|
|||||||
<Button>{children}</Button>
|
<Button>{children}</Button>
|
||||||
)}
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Redirects</DialogTitle>
|
<DialogTitle>Redirects</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Split, Trash2 } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -8,8 +10,6 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Split, Trash2 } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { HandleRedirect } from "./handle-redirect";
|
import { HandleRedirect } from "./handle-redirect";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -19,12 +25,6 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const AddSecuritychema = z.object({
|
const AddSecuritychema = z.object({
|
||||||
username: z.string().min(1, "Username is required"),
|
username: z.string().min(1, "Username is required"),
|
||||||
@@ -114,7 +114,7 @@ export const HandleSecurity = ({
|
|||||||
<Button>{children}</Button>
|
<Button>{children}</Button>
|
||||||
)}
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Security</DialogTitle>
|
<DialogTitle>Security</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -151,7 +151,7 @@ export const HandleSecurity = ({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Password</FormLabel>
|
<FormLabel>Password</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="test" {...field} />
|
<Input placeholder="test" type="password" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import { LockKeyhole, Trash2 } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -7,9 +10,9 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { LockKeyhole, Trash2 } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { HandleSecurity } from "./handle-security";
|
import { HandleSecurity } from "./handle-security";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -58,19 +61,18 @@ export const ShowSecurity = ({ applicationId }: Props) => {
|
|||||||
<div className="flex flex-col gap-6 ">
|
<div className="flex flex-col gap-6 ">
|
||||||
{data?.security.map((security) => (
|
{data?.security.map((security) => (
|
||||||
<div key={security.securityId}>
|
<div key={security.securityId}>
|
||||||
<div className="flex w-full flex-col sm:flex-row justify-between sm:items-center gap-4 sm:gap-10 border rounded-lg p-4">
|
<div className="flex w-full flex-col md:flex-row justify-between md:items-center gap-4 md:gap-10 border rounded-lg p-4">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 flex-col gap-4 sm:gap-8">
|
<div className="grid grid-cols-1 md:grid-cols-2 flex-col gap-4 md:gap-8">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-2">
|
||||||
<span className="font-medium">Username</span>
|
<Label>Username</Label>
|
||||||
<span className="text-sm text-muted-foreground">
|
<Input disabled value={security.username} />
|
||||||
{security.username}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-2">
|
||||||
<span className="font-medium">Password</span>
|
<Label>Password</Label>
|
||||||
<span className="text-sm text-muted-foreground">
|
<ToggleVisibilityInput
|
||||||
{security.password}
|
value={security.password}
|
||||||
</span>
|
disabled
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { InfoIcon } from "lucide-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -23,12 +29,6 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { InfoIcon } from "lucide-react";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const addResourcesSchema = z.object({
|
const addResourcesSchema = z.object({
|
||||||
memoryReservation: z.string().optional(),
|
memoryReservation: z.string().optional(),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { File, Loader2 } from "lucide-react";
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -7,8 +8,8 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { File, Loader2 } from "lucide-react";
|
|
||||||
import { UpdateTraefikConfig } from "./update-traefik-config";
|
import { UpdateTraefikConfig } from "./update-traefik-config";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import jsyaml from "js-yaml";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -19,12 +25,6 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import jsyaml from "js-yaml";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const UpdateTraefikConfigSchema = z.object({
|
const UpdateTraefikConfigSchema = z.object({
|
||||||
traefikConfig: z.string(),
|
traefikConfig: z.string(),
|
||||||
@@ -122,7 +122,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
|||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button isLoading={isLoading}>Modify</Button>
|
<Button isLoading={isLoading}>Modify</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
|
<DialogContent className="sm:max-w-4xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Update traefik config</DialogTitle>
|
<DialogTitle>Update traefik config</DialogTitle>
|
||||||
<DialogDescription>Update the traefik config</DialogDescription>
|
<DialogDescription>Update the traefik config</DialogDescription>
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { PlusIcon } from "lucide-react";
|
||||||
|
import type React from "react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -21,13 +29,7 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { PlusIcon } from "lucide-react";
|
|
||||||
import type React from "react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serviceId: string;
|
serviceId: string;
|
||||||
serviceType:
|
serviceType:
|
||||||
@@ -150,7 +152,7 @@ export const AddVolumes = ({
|
|||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
<Button>{children}</Button>
|
<Button>{children}</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-3xl">
|
<DialogContent className="sm:max-w-3xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Volumes / Mounts</DialogTitle>
|
<DialogTitle>Volumes / Mounts</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -169,6 +171,23 @@ export const AddVolumes = ({
|
|||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-8 "
|
className="grid w-full gap-8 "
|
||||||
>
|
>
|
||||||
|
{type === "bind" && (
|
||||||
|
<AlertBlock>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>
|
||||||
|
Make sure the host path is a valid path and exists in the
|
||||||
|
host machine.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
<strong>Cluster Warning:</strong> If you're using cluster
|
||||||
|
features, bind mounts may cause deployment failures since
|
||||||
|
the path must exist on all worker/manager nodes. Consider
|
||||||
|
using external tools to distribute the folder across nodes
|
||||||
|
or use named volumes instead.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
defaultValue={form.control._defaultValues.type}
|
defaultValue={form.control._defaultValues.type}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Package, Trash2 } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -9,11 +11,10 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Package, Trash2 } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import type { ServiceType } from "../show-resources";
|
import type { ServiceType } from "../show-resources";
|
||||||
import { AddVolumes } from "./add-volumes";
|
import { AddVolumes } from "./add-volumes";
|
||||||
import { UpdateVolume } from "./update-volume";
|
import { UpdateVolume } from "./update-volume";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
type: ServiceType | "compose";
|
type: ServiceType | "compose";
|
||||||
@@ -80,7 +81,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
|||||||
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
|
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
|
||||||
>
|
>
|
||||||
{/* <Package className="size-8 self-center text-muted-foreground" /> */}
|
{/* <Package className="size-8 self-center text-muted-foreground" /> */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 flex-col gap-4 sm:gap-8">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 flex-col gap-4 sm:gap-8">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="font-medium">Mount Type</span>
|
<span className="font-medium">Mount Type</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
@@ -112,21 +113,21 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{mount.type === "file" ? (
|
{mount.type === "file" && (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="font-medium">File Path</span>
|
<span className="font-medium">File Path</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{mount.filePath}
|
{mount.filePath}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<span className="font-medium">Mount Path</span>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{mount.mountPath}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="font-medium">Mount Path</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{mount.mountPath}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-1">
|
<div className="flex flex-row gap-1">
|
||||||
<UpdateVolume
|
<UpdateVolume
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { PenBoxIcon } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -20,12 +26,6 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { PenBoxIcon } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const mountSchema = z.object({
|
const mountSchema = z.object({
|
||||||
mountPath: z.string().min(1, "Mount path required"),
|
mountPath: z.string().min(1, "Mount path required"),
|
||||||
@@ -186,7 +186,7 @@ export const UpdateVolume = ({
|
|||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-3xl">
|
<DialogContent className="sm:max-w-3xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Update</DialogTitle>
|
<DialogTitle>Update</DialogTitle>
|
||||||
<DialogDescription>Update the mount</DialogDescription>
|
<DialogDescription>Update the mount</DialogDescription>
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Cog } from "lucide-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -15,12 +21,6 @@ import {
|
|||||||
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 { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { Cog } from "lucide-react";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export enum BuildType {
|
export enum BuildType {
|
||||||
dockerfile = "dockerfile",
|
dockerfile = "dockerfile",
|
||||||
@@ -65,6 +65,7 @@ const mySchema = z.discriminatedUnion("buildType", [
|
|||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
buildType: z.literal(BuildType.railpack),
|
buildType: z.literal(BuildType.railpack),
|
||||||
|
railpackVersion: z.string().nullable().default("0.2.2"),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
buildType: z.literal(BuildType.static),
|
buildType: z.literal(BuildType.static),
|
||||||
@@ -86,6 +87,7 @@ interface ApplicationData {
|
|||||||
herokuVersion?: string | null;
|
herokuVersion?: string | null;
|
||||||
publishDirectory?: string | null;
|
publishDirectory?: string | null;
|
||||||
isStaticSpa?: boolean | null;
|
isStaticSpa?: boolean | null;
|
||||||
|
railpackVersion?: string | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidBuildType(value: string): value is BuildType {
|
function isValidBuildType(value: string): value is BuildType {
|
||||||
@@ -123,6 +125,7 @@ const resetData = (data: ApplicationData): AddTemplate => {
|
|||||||
case BuildType.railpack:
|
case BuildType.railpack:
|
||||||
return {
|
return {
|
||||||
buildType: BuildType.railpack,
|
buildType: BuildType.railpack,
|
||||||
|
railpackVersion: data.railpackVersion || null,
|
||||||
};
|
};
|
||||||
default: {
|
default: {
|
||||||
const buildType = data.buildType as BuildType;
|
const buildType = data.buildType as BuildType;
|
||||||
@@ -181,6 +184,10 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
: null,
|
: null,
|
||||||
isStaticSpa:
|
isStaticSpa:
|
||||||
data.buildType === BuildType.static ? data.isStaticSpa : null,
|
data.buildType === BuildType.static ? data.isStaticSpa : null,
|
||||||
|
railpackVersion:
|
||||||
|
data.buildType === BuildType.railpack
|
||||||
|
? data.railpackVersion || "0.2.2"
|
||||||
|
: null,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Build type saved");
|
toast.success("Build type saved");
|
||||||
@@ -395,6 +402,25 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{buildType === BuildType.railpack && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="railpackVersion"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Railpack Version</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Railpack Version"
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
<Button isLoading={isLoading} type="submit">
|
<Button isLoading={isLoading} type="submit">
|
||||||
Save
|
Save
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Paintbrush } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -11,8 +13,6 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Paintbrush } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { RefreshCcw } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -10,8 +12,6 @@ import {
|
|||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { RefreshCcw } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import {
|
import {
|
||||||
@@ -7,8 +9,6 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { TerminalLine } from "../../docker/logs/terminal-line";
|
import { TerminalLine } from "../../docker/logs/terminal-line";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ export const ShowDeployment = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
<DialogContent className={"sm:max-w-5xl"}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Deployment</DialogTitle>
|
<DialogTitle>Deployment</DialogTitle>
|
||||||
<DialogDescription className="flex items-center gap-2">
|
<DialogDescription className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@/utils/api";
|
import type { RouterOutputs } from "@/utils/api";
|
||||||
import { useState } from "react";
|
|
||||||
import { ShowDeployment } from "../deployments/show-deployment";
|
import { ShowDeployment } from "../deployments/show-deployment";
|
||||||
import { ShowDeployments } from "./show-deployments";
|
import { ShowDeployments } from "./show-deployments";
|
||||||
|
|
||||||
@@ -14,7 +13,8 @@ interface Props {
|
|||||||
| "schedule"
|
| "schedule"
|
||||||
| "server"
|
| "server"
|
||||||
| "backup"
|
| "backup"
|
||||||
| "previewDeployment";
|
| "previewDeployment"
|
||||||
|
| "volumeBackup";
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
refreshToken?: string;
|
refreshToken?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
@@ -49,7 +49,7 @@ export const ShowDeploymentsModal = ({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl p-0">
|
<DialogContent className="sm:max-w-5xl p-0">
|
||||||
<ShowDeployments
|
<ShowDeployments
|
||||||
id={id}
|
id={id}
|
||||||
type={type}
|
type={type}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
import { Clock, Loader2, RefreshCcw, RocketIcon, Settings } from "lucide-react";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -9,15 +13,11 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { type RouterOutputs, api } from "@/utils/api";
|
import { api, type RouterOutputs } from "@/utils/api";
|
||||||
import { Clock, Loader2, RocketIcon, Settings, RefreshCcw } from "lucide-react";
|
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { CancelQueues } from "./cancel-queues";
|
import { CancelQueues } from "./cancel-queues";
|
||||||
import { RefreshToken } from "./refresh-token";
|
import { RefreshToken } from "./refresh-token";
|
||||||
import { ShowDeployment } from "./show-deployment";
|
import { ShowDeployment } from "./show-deployment";
|
||||||
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
|
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -27,7 +27,8 @@ interface Props {
|
|||||||
| "schedule"
|
| "schedule"
|
||||||
| "server"
|
| "server"
|
||||||
| "backup"
|
| "backup"
|
||||||
| "previewDeployment";
|
| "previewDeployment"
|
||||||
|
| "volumeBackup";
|
||||||
refreshToken?: string;
|
refreshToken?: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
}
|
}
|
||||||
@@ -62,6 +63,8 @@ export const ShowDeployments = ({
|
|||||||
|
|
||||||
const { mutateAsync: rollback, isLoading: isRollingBack } =
|
const { mutateAsync: rollback, isLoading: isRollingBack } =
|
||||||
api.rollback.rollback.useMutation();
|
api.rollback.rollback.useMutation();
|
||||||
|
const { mutateAsync: killProcess, isLoading: isKillingProcess } =
|
||||||
|
api.deployment.killProcess.useMutation();
|
||||||
|
|
||||||
const [url, setUrl] = React.useState("");
|
const [url, setUrl] = React.useState("");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -170,6 +173,32 @@ export const ShowDeployments = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
{deployment.pid && deployment.status === "running" && (
|
||||||
|
<DialogAction
|
||||||
|
title="Kill Process"
|
||||||
|
description="Are you sure you want to kill the process?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await killProcess({
|
||||||
|
deploymentId: deployment.deploymentId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Process killed successfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error killing process");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
isLoading={isKillingProcess}
|
||||||
|
>
|
||||||
|
Kill Process
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveLog(deployment);
|
setActiveLog(deployment);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Copy, HelpCircle, Server } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -8,8 +10,6 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Copy, HelpCircle, Server } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
domain: {
|
domain: {
|
||||||
@@ -33,7 +33,7 @@ export const DnsHelperModal = ({ domain, serverIp }: Props) => {
|
|||||||
<HelpCircle className="size-4" />
|
<HelpCircle className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<Server className="size-5" />
|
<Server className="size-5" />
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import z from "zod";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -34,14 +41,6 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import z from "zod";
|
|
||||||
|
|
||||||
export type CacheType = "fetch" | "cache";
|
export type CacheType = "fetch" | "cache";
|
||||||
|
|
||||||
@@ -49,6 +48,8 @@ export const domain = z
|
|||||||
.object({
|
.object({
|
||||||
host: z.string().min(1, { message: "Add a hostname" }),
|
host: z.string().min(1, { message: "Add a hostname" }),
|
||||||
path: z.string().min(1).optional(),
|
path: z.string().min(1).optional(),
|
||||||
|
internalPath: z.string().optional(),
|
||||||
|
stripPath: z.boolean().optional(),
|
||||||
port: z
|
port: z
|
||||||
.number()
|
.number()
|
||||||
.min(1, { message: "Port must be at least 1" })
|
.min(1, { message: "Port must be at least 1" })
|
||||||
@@ -84,6 +85,29 @@ export const domain = z
|
|||||||
message: "Required",
|
message: "Required",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate stripPath requires a valid path
|
||||||
|
if (input.stripPath && (!input.path || input.path === "/")) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["stripPath"],
|
||||||
|
message:
|
||||||
|
"Strip path can only be enabled when a path other than '/' is specified",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate internalPath starts with /
|
||||||
|
if (
|
||||||
|
input.internalPath &&
|
||||||
|
input.internalPath !== "/" &&
|
||||||
|
!input.internalPath.startsWith("/")
|
||||||
|
) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["internalPath"],
|
||||||
|
message: "Internal path must start with '/'",
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
type Domain = z.infer<typeof domain>;
|
type Domain = z.infer<typeof domain>;
|
||||||
@@ -98,6 +122,7 @@ interface Props {
|
|||||||
export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
||||||
|
const [isManualInput, setIsManualInput] = useState(false);
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data, refetch } = api.domain.one.useQuery(
|
const { data, refetch } = api.domain.one.useQuery(
|
||||||
@@ -162,6 +187,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
host: "",
|
host: "",
|
||||||
path: undefined,
|
path: undefined,
|
||||||
|
internalPath: undefined,
|
||||||
|
stripPath: false,
|
||||||
port: undefined,
|
port: undefined,
|
||||||
https: false,
|
https: false,
|
||||||
certificateType: undefined,
|
certificateType: undefined,
|
||||||
@@ -182,6 +209,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
...data,
|
...data,
|
||||||
/* Convert null to undefined */
|
/* Convert null to undefined */
|
||||||
path: data?.path || undefined,
|
path: data?.path || undefined,
|
||||||
|
internalPath: data?.internalPath || undefined,
|
||||||
|
stripPath: data?.stripPath || false,
|
||||||
port: data?.port || undefined,
|
port: data?.port || undefined,
|
||||||
certificateType: data?.certificateType || undefined,
|
certificateType: data?.certificateType || undefined,
|
||||||
customCertResolver: data?.customCertResolver || undefined,
|
customCertResolver: data?.customCertResolver || undefined,
|
||||||
@@ -194,6 +223,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
form.reset({
|
form.reset({
|
||||||
host: "",
|
host: "",
|
||||||
path: undefined,
|
path: undefined,
|
||||||
|
internalPath: undefined,
|
||||||
|
stripPath: false,
|
||||||
port: undefined,
|
port: undefined,
|
||||||
https: false,
|
https: false,
|
||||||
certificateType: undefined,
|
certificateType: undefined,
|
||||||
@@ -261,7 +292,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
{children}
|
{children}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Domain</DialogTitle>
|
<DialogTitle>Domain</DialogTitle>
|
||||||
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||||
@@ -294,46 +325,126 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
<FormItem className="w-full">
|
<FormItem className="w-full">
|
||||||
<FormLabel>Service Name</FormLabel>
|
<FormLabel>Service Name</FormLabel>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Select
|
{isManualInput ? (
|
||||||
onValueChange={field.onChange}
|
|
||||||
defaultValue={field.value || ""}
|
|
||||||
>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<Input
|
||||||
<SelectValue placeholder="Select a service name" />
|
placeholder="Enter service name manually"
|
||||||
</SelectTrigger>
|
{...field}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
) : (
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value || ""}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a service name" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{services?.map((service, index) => (
|
{services?.map((service, index) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
value={service}
|
value={service}
|
||||||
key={`${service}-${index}`}
|
key={`${service}-${index}`}
|
||||||
>
|
>
|
||||||
{service}
|
{service}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectItem value="none" disabled>
|
||||||
|
Empty
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
</SelectContent>
|
||||||
<SelectItem value="none" disabled>
|
</Select>
|
||||||
Empty
|
)}
|
||||||
</SelectItem>
|
{!isManualInput && (
|
||||||
</SelectContent>
|
<>
|
||||||
</Select>
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingServices}
|
||||||
|
onClick={() => {
|
||||||
|
if (cacheType === "fetch") {
|
||||||
|
refetchServices();
|
||||||
|
} else {
|
||||||
|
setCacheType("fetch");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Fetch: Will clone the repository and
|
||||||
|
load the services
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingServices}
|
||||||
|
onClick={() => {
|
||||||
|
if (cacheType === "cache") {
|
||||||
|
refetchServices();
|
||||||
|
} else {
|
||||||
|
setCacheType("cache");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DatabaseZap className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Cache: If you previously deployed this
|
||||||
|
compose, it will read the services
|
||||||
|
from the last deployment/fetch from
|
||||||
|
the repository
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
isLoading={isLoadingServices}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (cacheType === "fetch") {
|
setIsManualInput(!isManualInput);
|
||||||
refetchServices();
|
if (!isManualInput) {
|
||||||
} else {
|
field.onChange("");
|
||||||
setCacheType("fetch");
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RefreshCw className="size-4 text-muted-foreground" />
|
{isManualInput ? (
|
||||||
|
<RefreshCw className="size-4 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Manual
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent
|
<TooltipContent
|
||||||
@@ -342,40 +453,9 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
className="max-w-[10rem]"
|
className="max-w-[10rem]"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
Fetch: Will clone the repository and load
|
{isManualInput
|
||||||
the services
|
? "Switch to service selection"
|
||||||
</p>
|
: "Enter service name manually"}
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
type="button"
|
|
||||||
isLoading={isLoadingServices}
|
|
||||||
onClick={() => {
|
|
||||||
if (cacheType === "cache") {
|
|
||||||
refetchServices();
|
|
||||||
} else {
|
|
||||||
setCacheType("cache");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DatabaseZap className="size-4 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent
|
|
||||||
side="left"
|
|
||||||
sideOffset={5}
|
|
||||||
className="max-w-[10rem]"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
Cache: If you previously deployed this
|
|
||||||
compose, it will read the services from
|
|
||||||
the last deployment/fetch from the
|
|
||||||
repository
|
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -469,6 +549,49 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="internalPath"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Internal Path</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
The path where your application expects to receive
|
||||||
|
requests internally (defaults to "/")
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder={"/"} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="stripPath"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between p-3 border rounded-lg shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Strip Path</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Remove the external path from the request before
|
||||||
|
forwarding to the application
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="port"
|
name="port"
|
||||||
|
|||||||
@@ -1,3 +1,18 @@
|
|||||||
|
import {
|
||||||
|
CheckCircle2,
|
||||||
|
ExternalLink,
|
||||||
|
GlobeIcon,
|
||||||
|
InfoIcon,
|
||||||
|
Loader2,
|
||||||
|
PenBoxIcon,
|
||||||
|
RefreshCw,
|
||||||
|
Server,
|
||||||
|
Trash2,
|
||||||
|
XCircle,
|
||||||
|
} from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -15,21 +30,6 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import {
|
|
||||||
CheckCircle2,
|
|
||||||
ExternalLink,
|
|
||||||
GlobeIcon,
|
|
||||||
InfoIcon,
|
|
||||||
Loader2,
|
|
||||||
PenBoxIcon,
|
|
||||||
RefreshCw,
|
|
||||||
Server,
|
|
||||||
Trash2,
|
|
||||||
XCircle,
|
|
||||||
} from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { DnsHelperModal } from "./dns-helper-modal";
|
import { DnsHelperModal } from "./dns-helper-modal";
|
||||||
import { AddDomain } from "./handle-domain";
|
import { AddDomain } from "./handle-domain";
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||||
|
import { type CSSProperties, useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -16,12 +22,6 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Toggle } from "@/components/ui/toggle";
|
import { Toggle } from "@/components/ui/toggle";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
|
||||||
import { type CSSProperties, useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
import type { ServiceType } from "../advanced/show-resources";
|
import type { ServiceType } from "../advanced/show-resources";
|
||||||
|
|
||||||
const addEnvironmentSchema = z.object({
|
const addEnvironmentSchema = z.object({
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card } from "@/components/ui/card";
|
|
||||||
import { Form } from "@/components/ui/form";
|
|
||||||
import { Secrets } from "@/components/ui/secrets";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
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 { Button } from "@/components/ui/button";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { Form } from "@/components/ui/form";
|
||||||
|
import { Secrets } from "@/components/ui/secrets";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
const addEnvironmentSchema = z.object({
|
const addEnvironmentSchema = z.object({
|
||||||
env: z.string(),
|
env: z.string(),
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -40,13 +47,6 @@ import {
|
|||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const BitbucketProviderSchema = z.object({
|
const BitbucketProviderSchema = z.object({
|
||||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -9,11 +14,6 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const DockerProviderSchema = z.object({
|
const DockerProviderSchema = z.object({
|
||||||
dockerImage: z.string().min(1, {
|
dockerImage: z.string().min(1, {
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { TrashIcon } from "lucide-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Dropzone } from "@/components/ui/dropzone";
|
import { Dropzone } from "@/components/ui/dropzone";
|
||||||
import {
|
import {
|
||||||
@@ -11,11 +16,6 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { type UploadFile, uploadFileSchema } from "@/utils/schema";
|
import { type UploadFile, uploadFileSchema } from "@/utils/schema";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { TrashIcon } from "lucide-react";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { GitIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -25,17 +35,6 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
import { GitIcon } from "@/components/icons/data-tools-icons";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const GitProviderSchema = z.object({
|
const GitProviderSchema = z.object({
|
||||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
import { GiteaIcon } from "@/components/icons/data-tools-icons";
|
import { GiteaIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -40,13 +47,6 @@ import {
|
|||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
interface GiteaRepository {
|
interface GiteaRepository {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -39,13 +46,6 @@ import {
|
|||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const GithubProviderSchema = z.object({
|
const GithubProviderSchema = z.object({
|
||||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -40,13 +47,6 @@ import {
|
|||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const GitlabProviderSchema = z.object({
|
const GitlabProviderSchema = z.object({
|
||||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||||
@@ -96,6 +96,16 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
const repository = form.watch("repository");
|
const repository = form.watch("repository");
|
||||||
const gitlabId = form.watch("gitlabId");
|
const gitlabId = form.watch("gitlabId");
|
||||||
|
|
||||||
|
const gitlabUrl = useMemo(() => {
|
||||||
|
const url = gitlabProviders?.find(
|
||||||
|
(provider) => provider.gitlabId === gitlabId,
|
||||||
|
)?.gitlabUrl;
|
||||||
|
|
||||||
|
const gitlabUrl = url?.replace(/\/$/, "");
|
||||||
|
|
||||||
|
return gitlabUrl || "https://gitlab.com";
|
||||||
|
}, [gitlabId, gitlabProviders]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: repositories,
|
data: repositories,
|
||||||
isLoading: isLoadingRepositories,
|
isLoading: isLoadingRepositories,
|
||||||
@@ -224,7 +234,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
<FormLabel>Repository</FormLabel>
|
<FormLabel>Repository</FormLabel>
|
||||||
{field.value.owner && field.value.repo && (
|
{field.value.owner && field.value.repo && (
|
||||||
<Link
|
<Link
|
||||||
href={`https://gitlab.com/${field.value.owner}/${field.value.repo}`}
|
href={`${gitlabUrl}/${field.value.owner}/${field.value.repo}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||||
@@ -278,7 +288,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
{repositories?.map((repo) => {
|
{repositories?.map((repo) => {
|
||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={repo.name}
|
value={repo.url}
|
||||||
key={repo.url}
|
key={repo.url}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
form.setValue("repository", {
|
form.setValue("repository", {
|
||||||
@@ -299,7 +309,8 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
repo.name === field.value.repo
|
repo.url ===
|
||||||
|
field.value.gitlabPathNamespace
|
||||||
? "opacity-100"
|
? "opacity-100"
|
||||||
: "opacity-0",
|
: "opacity-0",
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import { GitBranch, Loader2, UploadCloud } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { SaveDockerProvider } from "@/components/dashboard/application/general/generic/save-docker-provider";
|
import { SaveDockerProvider } from "@/components/dashboard/application/general/generic/save-docker-provider";
|
||||||
import { SaveGitProvider } from "@/components/dashboard/application/general/generic/save-git-provider";
|
import { SaveGitProvider } from "@/components/dashboard/application/general/generic/save-git-provider";
|
||||||
import { SaveGiteaProvider } from "@/components/dashboard/application/general/generic/save-gitea-provider";
|
import { SaveGiteaProvider } from "@/components/dashboard/application/general/generic/save-gitea-provider";
|
||||||
@@ -5,18 +9,14 @@ import { SaveGithubProvider } from "@/components/dashboard/application/general/g
|
|||||||
import {
|
import {
|
||||||
BitbucketIcon,
|
BitbucketIcon,
|
||||||
DockerIcon,
|
DockerIcon,
|
||||||
GitIcon,
|
|
||||||
GiteaIcon,
|
GiteaIcon,
|
||||||
GithubIcon,
|
GithubIcon,
|
||||||
|
GitIcon,
|
||||||
GitlabIcon,
|
GitlabIcon,
|
||||||
} from "@/components/icons/data-tools-icons";
|
} from "@/components/icons/data-tools-icons";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { GitBranch, Loader2, UploadCloud } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { SaveBitbucketProvider } from "./save-bitbucket-provider";
|
import { SaveBitbucketProvider } from "./save-bitbucket-provider";
|
||||||
import { SaveDragNDrop } from "./save-drag-n-drop";
|
import { SaveDragNDrop } from "./save-drag-n-drop";
|
||||||
import { SaveGitlabProvider } from "./save-gitlab-provider";
|
import { SaveGitlabProvider } from "./save-gitlab-provider";
|
||||||
@@ -153,8 +153,8 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
setSab(e as TabState);
|
setSab(e as TabState);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
<div className="flex flex-row items-center justify-between w-full overflow-auto">
|
||||||
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
|
<TabsList className="flex gap-4 justify-start bg-transparent">
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="github"
|
value="github"
|
||||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { AlertCircle, GitBranch, Unlink } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
BitbucketIcon,
|
BitbucketIcon,
|
||||||
GitIcon,
|
|
||||||
GiteaIcon,
|
GiteaIcon,
|
||||||
GithubIcon,
|
GithubIcon,
|
||||||
|
GitIcon,
|
||||||
GitlabIcon,
|
GitlabIcon,
|
||||||
} from "@/components/icons/data-tools-icons";
|
} from "@/components/icons/data-tools-icons";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
@@ -10,7 +11,6 @@ import { Alert, AlertDescription } from "@/components/ui/alert";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import type { RouterOutputs } from "@/utils/api";
|
import type { RouterOutputs } from "@/utils/api";
|
||||||
import { AlertCircle, GitBranch, Unlink } from "lucide-react";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
service:
|
service:
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
|
import {
|
||||||
|
Ban,
|
||||||
|
CheckCircle2,
|
||||||
|
Hammer,
|
||||||
|
RefreshCcw,
|
||||||
|
Rocket,
|
||||||
|
Terminal,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { ShowBuildChooseForm } from "@/components/dashboard/application/build/show";
|
import { ShowBuildChooseForm } from "@/components/dashboard/application/build/show";
|
||||||
import { ShowProviderForm } from "@/components/dashboard/application/general/generic/show";
|
import { ShowProviderForm } from "@/components/dashboard/application/general/generic/show";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
@@ -11,18 +22,8 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
||||||
import {
|
|
||||||
Ban,
|
|
||||||
CheckCircle2,
|
|
||||||
Hammer,
|
|
||||||
RefreshCcw,
|
|
||||||
Rocket,
|
|
||||||
Terminal,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
@@ -78,7 +79,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
isLoading={data?.applicationStatus === "running"}
|
// isLoading={data?.applicationStatus === "running"}
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -18,9 +21,6 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
export const DockerLogs = dynamic(
|
export const DockerLogs = dynamic(
|
||||||
() =>
|
() =>
|
||||||
import("@/components/dashboard/docker/logs/docker-logs-id").then(
|
import("@/components/dashboard/docker/logs/docker-logs-id").then(
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Dices } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import type z from "zod";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -33,15 +39,8 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
import { domain } from "@/server/db/validations/domain";
|
import { domain } from "@/server/db/validations/domain";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { api } from "@/utils/api";
|
||||||
import { Dices } from "lucide-react";
|
|
||||||
import type z from "zod";
|
|
||||||
|
|
||||||
type Domain = z.infer<typeof domain>;
|
type Domain = z.infer<typeof domain>;
|
||||||
|
|
||||||
@@ -138,7 +137,7 @@ export const AddPreviewDomain = ({
|
|||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
{children}
|
{children}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Domain</DialogTitle>
|
<DialogTitle>Domain</DialogTitle>
|
||||||
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
import {
|
||||||
|
ExternalLink,
|
||||||
|
FileText,
|
||||||
|
GitPullRequest,
|
||||||
|
Loader2,
|
||||||
|
PenSquare,
|
||||||
|
RocketIcon,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
@@ -13,16 +23,6 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import {
|
|
||||||
ExternalLink,
|
|
||||||
FileText,
|
|
||||||
GitPullRequest,
|
|
||||||
Loader2,
|
|
||||||
PenSquare,
|
|
||||||
RocketIcon,
|
|
||||||
Trash2,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
|
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
|
||||||
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
|
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
|
||||||
import { AddPreviewDomain } from "./add-preview-domain";
|
import { AddPreviewDomain } from "./add-preview-domain";
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { HelpCircle, Plus, Settings2, X } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -27,13 +34,13 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { Settings2 } 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
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -42,10 +49,12 @@ const schema = z
|
|||||||
wildcardDomain: z.string(),
|
wildcardDomain: z.string(),
|
||||||
port: z.number(),
|
port: z.number(),
|
||||||
previewLimit: z.number(),
|
previewLimit: z.number(),
|
||||||
|
previewLabels: z.array(z.string()).optional(),
|
||||||
previewHttps: z.boolean(),
|
previewHttps: z.boolean(),
|
||||||
previewPath: z.string(),
|
previewPath: z.string(),
|
||||||
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]),
|
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]),
|
||||||
previewCustomCertResolver: z.string().optional(),
|
previewCustomCertResolver: z.string().optional(),
|
||||||
|
previewRequireCollaboratorPermissions: z.boolean(),
|
||||||
})
|
})
|
||||||
.superRefine((input, ctx) => {
|
.superRefine((input, ctx) => {
|
||||||
if (
|
if (
|
||||||
@@ -80,9 +89,11 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
wildcardDomain: "*.traefik.me",
|
wildcardDomain: "*.traefik.me",
|
||||||
port: 3000,
|
port: 3000,
|
||||||
previewLimit: 3,
|
previewLimit: 3,
|
||||||
|
previewLabels: [],
|
||||||
previewHttps: false,
|
previewHttps: false,
|
||||||
previewPath: "/",
|
previewPath: "/",
|
||||||
previewCertificateType: "none",
|
previewCertificateType: "none",
|
||||||
|
previewRequireCollaboratorPermissions: true,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
});
|
});
|
||||||
@@ -100,11 +111,14 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
buildArgs: data.previewBuildArgs || "",
|
buildArgs: data.previewBuildArgs || "",
|
||||||
wildcardDomain: data.previewWildcard || "*.traefik.me",
|
wildcardDomain: data.previewWildcard || "*.traefik.me",
|
||||||
port: data.previewPort || 3000,
|
port: data.previewPort || 3000,
|
||||||
|
previewLabels: data.previewLabels || [],
|
||||||
previewLimit: data.previewLimit || 3,
|
previewLimit: data.previewLimit || 3,
|
||||||
previewHttps: data.previewHttps || false,
|
previewHttps: data.previewHttps || false,
|
||||||
previewPath: data.previewPath || "/",
|
previewPath: data.previewPath || "/",
|
||||||
previewCertificateType: data.previewCertificateType || "none",
|
previewCertificateType: data.previewCertificateType || "none",
|
||||||
previewCustomCertResolver: data.previewCustomCertResolver || "",
|
previewCustomCertResolver: data.previewCustomCertResolver || "",
|
||||||
|
previewRequireCollaboratorPermissions:
|
||||||
|
data.previewRequireCollaboratorPermissions || true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
@@ -115,12 +129,15 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
previewBuildArgs: formData.buildArgs,
|
previewBuildArgs: formData.buildArgs,
|
||||||
previewWildcard: formData.wildcardDomain,
|
previewWildcard: formData.wildcardDomain,
|
||||||
previewPort: formData.port,
|
previewPort: formData.port,
|
||||||
|
previewLabels: formData.previewLabels,
|
||||||
applicationId,
|
applicationId,
|
||||||
previewLimit: formData.previewLimit,
|
previewLimit: formData.previewLimit,
|
||||||
previewHttps: formData.previewHttps,
|
previewHttps: formData.previewHttps,
|
||||||
previewPath: formData.previewPath,
|
previewPath: formData.previewPath,
|
||||||
previewCertificateType: formData.previewCertificateType,
|
previewCertificateType: formData.previewCertificateType,
|
||||||
previewCustomCertResolver: formData.previewCustomCertResolver,
|
previewCustomCertResolver: formData.previewCustomCertResolver,
|
||||||
|
previewRequireCollaboratorPermissions:
|
||||||
|
formData.previewRequireCollaboratorPermissions,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Preview Deployments settings updated");
|
toast.success("Preview Deployments settings updated");
|
||||||
@@ -138,7 +155,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
Configure
|
Configure
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl w-full">
|
<DialogContent className="sm:max-w-5xl w-full">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Preview Deployment Settings</DialogTitle>
|
<DialogTitle>Preview Deployment Settings</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -194,6 +211,90 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="previewLabels"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>Preview Labels</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Add a labels that will trigger a preview
|
||||||
|
deployment for a pull request. If no labels
|
||||||
|
are specified, all pull requests will trigger
|
||||||
|
a preview deployment.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-2">
|
||||||
|
{field.value?.map((label, index) => (
|
||||||
|
<Badge
|
||||||
|
key={index}
|
||||||
|
variant="secondary"
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
<X
|
||||||
|
className="size-3 cursor-pointer hover:text-destructive"
|
||||||
|
onClick={() => {
|
||||||
|
const newLabels = [...(field.value || [])];
|
||||||
|
newLabels.splice(index, 1);
|
||||||
|
field.onChange(newLabels);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter a label (e.g. enhancements, needs-review)"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
const input = e.currentTarget;
|
||||||
|
const label = input.value.trim();
|
||||||
|
if (label) {
|
||||||
|
field.onChange([
|
||||||
|
...(field.value || []),
|
||||||
|
label,
|
||||||
|
]);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => {
|
||||||
|
const input = document.querySelector(
|
||||||
|
'input[placeholder*="Enter a label"]',
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const label = input.value.trim();
|
||||||
|
if (label) {
|
||||||
|
field.onChange([...(field.value || []), label]);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="previewLimit"
|
name="previewLimit"
|
||||||
@@ -312,6 +413,37 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 lg:grid-cols-2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="previewRequireCollaboratorPermissions"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm col-span-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>
|
||||||
|
Require Collaborator Permissions
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Require collaborator permissions to preview
|
||||||
|
deployments, valid roles are:
|
||||||
|
<ul>
|
||||||
|
<li>Admin</li>
|
||||||
|
<li>Maintain</li>
|
||||||
|
<li>Write</li>
|
||||||
|
</ul>
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="env"
|
name="env"
|
||||||
|
|||||||
108
apps/dokploy/components/dashboard/application/rollbacks/Backup
Normal file
108
apps/dokploy/components/dashboard/application/rollbacks/Backup
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
Backup
|
||||||
|
# license-namedbackups-abxelc
|
||||||
|
1. docker ps --filter "label=com.docker.swarm.service.name=license-namedbackups-abxelc" --format "{{.Names}}"
|
||||||
|
2. docker run --rm \
|
||||||
|
--volumes-from "license-namedbackups-abxelc.1.m3cxy78ocj3w0zu42kmgamc5y" \
|
||||||
|
-v $(pwd):/backup \
|
||||||
|
ubuntu \
|
||||||
|
tar cvf /backup/backup.tar /var/lib/postgresql/data
|
||||||
|
|
||||||
|
|
||||||
|
# Official Command Backup
|
||||||
|
|
||||||
|
1. Backup
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
-v license-namedbackups-abxelc-data:/volume_data \
|
||||||
|
-v $(pwd):/backup \
|
||||||
|
ubuntu \
|
||||||
|
bash -c "cd /volume_data && tar cvf /backup/generic_backup.tar ."
|
||||||
|
|
||||||
|
|
||||||
|
2. Restore
|
||||||
|
|
||||||
|
docker service scale license-namedbackups-abxelc=0
|
||||||
|
|
||||||
|
docker volume rm license-namedbackups-abxelc-data
|
||||||
|
|
||||||
|
2. docker run --rm \
|
||||||
|
-v license-namedbackups-abxelc-data:/volume_data \
|
||||||
|
-v $(pwd):/backup \
|
||||||
|
ubuntu \
|
||||||
|
bash -c "cd /volume_data && tar xvf /backup/generic_backup.tar ."
|
||||||
|
|
||||||
|
docker service scale license-namedbackups-abxelc=1
|
||||||
|
|
||||||
|
|
||||||
|
root@srv594061:~# docker volume inspect n8n_data-data
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"CreatedAt": "2025-06-28T18:07:44Z",
|
||||||
|
"Driver": "local",
|
||||||
|
"Labels": null,
|
||||||
|
"Mountpoint": "/var/lib/docker/volumes/n8n_data-data/_data",
|
||||||
|
"Name": "n8n_data-data",
|
||||||
|
"Options": null,
|
||||||
|
"Scope": "local"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
Archivos funcuionando creados por N8N
|
||||||
|
|
||||||
|
# root@srv594061:~# cd /var/lib/docker/volumes/n8n_data-data/_data
|
||||||
|
# root@srv594061:/var/lib/docker/volumes/n8n_data-data/_data# ls
|
||||||
|
# binaryData config crash.journal database.sqlite git n8nEventLog.log ssh
|
||||||
|
|
||||||
|
Luego que intente hacer el backup con el comando de backup
|
||||||
|
|
||||||
|
|
||||||
|
root@srv594061:~# docker run --rm -v n8n_data-data:/volume_data -v $(pwd):/backup ubuntu bash -c "cd /volume_data && tar cvf /backup/generic_backup6.tar ."
|
||||||
|
./
|
||||||
|
./config
|
||||||
|
./crash.journal
|
||||||
|
./binaryData/
|
||||||
|
./git/
|
||||||
|
./database.sqlite
|
||||||
|
./ssh/
|
||||||
|
./n8nEventLog.log
|
||||||
|
root@srv594061:~#
|
||||||
|
|
||||||
|
# Paramos la aplicacion
|
||||||
|
docker service scale n8n=0
|
||||||
|
|
||||||
|
# Haciendo el restore
|
||||||
|
root@srv594061:~# docker volume rm n8n_data-data
|
||||||
|
n8n_data-data
|
||||||
|
root@srv594061:~# docker run --rm -v n8n_data-data:/volume_data -v $(pwd):/backup ubuntu bash -c "cd /volume_data && tar xvf /backup/generic_backup6.tar && chown -R 999:999 ."
|
||||||
|
./
|
||||||
|
./config
|
||||||
|
./crash.journal
|
||||||
|
./binaryData/
|
||||||
|
./git/
|
||||||
|
./database.sqlite
|
||||||
|
./ssh/
|
||||||
|
./n8nEventLog.log
|
||||||
|
|
||||||
|
# Tenemos los archivos en el volumen
|
||||||
|
root@srv594061:~# ls /var/lib/docker/volumes/n8n_data-data/_data
|
||||||
|
binaryData config crash.journal database.sqlite git n8nEventLog.log ssh
|
||||||
|
root@srv594061:~#
|
||||||
|
|
||||||
|
docker service scale n8n=1
|
||||||
|
|
||||||
|
# Luego en N8N Cuando se que el volumen tiene la data
|
||||||
|
Permissions 0644 for n8n settings file /home/node/.n8n/config are too wide. This is ignored for now, but in the future n8n will attempt to change the permissions automatically. To automatically enforce correct permissions now set N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true (recommended), or turn this check off set N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=false.
|
||||||
|
User settings loaded from: /home/node/.n8n/config
|
||||||
|
Last session crashed
|
||||||
|
Error: EACCES: permission denied, open '/home/node/.n8n/crash.journal'
|
||||||
|
at open (node:internal/fs/promises:639:25)
|
||||||
|
at touchFile (/usr/local/lib/node_modules/n8n/dist/crash-journal.js:18:20)
|
||||||
|
at Object.init (/usr/local/lib/node_modules/n8n/dist/crash-journal.js:32:5)
|
||||||
|
at Start.initCrashJournal (/usr/local/lib/node_modules/n8n/dist/commands/base-command.js:113:9)
|
||||||
|
at Start.init (/usr/local/lib/node_modules/n8n/dist/commands/start.js:141:9)
|
||||||
|
at Start._run (/usr/local/lib/node_modules/n8n/node_modules/@oclif/core/lib/command.js:301:13)
|
||||||
|
at Config.runCommand (/usr/local/lib/node_modules/n8n/node_modules/@oclif/core/lib/config/config.js:424:25)
|
||||||
|
at run (/usr/local/lib/node_modules/n8n/node_modules/@oclif/core/lib/main.js:94:16)
|
||||||
|
at /usr/local/lib/node_modules/n8n/bin/n8n:71:2
|
||||||
|
TypeError: Cannot read properties of undefined (reading 'error')
|
||||||
|
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -17,11 +23,6 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
rollbackActive: z.boolean(),
|
rollbackActive: z.boolean(),
|
||||||
@@ -79,6 +80,11 @@ export const ShowRollbackSettings = ({ applicationId, children }: Props) => {
|
|||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Configure how rollbacks work for this application
|
Configure how rollbacks work for this application
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
|
<AlertBlock>
|
||||||
|
Having rollbacks enabled increases storage usage. Be careful with
|
||||||
|
this option. Note that manually cleaning the cache may delete
|
||||||
|
rollback images, making them unavailable for future rollbacks.
|
||||||
|
</AlertBlock>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import {
|
||||||
|
DatabaseZap,
|
||||||
|
Info,
|
||||||
|
PenBoxIcon,
|
||||||
|
PlusCircle,
|
||||||
|
RefreshCw,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
@@ -34,18 +47,6 @@ import {
|
|||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import {
|
|
||||||
DatabaseZap,
|
|
||||||
Info,
|
|
||||||
PenBoxIcon,
|
|
||||||
PlusCircle,
|
|
||||||
RefreshCw,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
import type { CacheType } from "../domains/handle-domain";
|
import type { CacheType } from "../domains/handle-domain";
|
||||||
|
|
||||||
export const commonCronExpressions = [
|
export const commonCronExpressions = [
|
||||||
@@ -232,14 +233,17 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className={cn(
|
className={cn(
|
||||||
"max-h-screen overflow-y-auto",
|
|
||||||
scheduleTypeForm === "dokploy-server" || scheduleTypeForm === "server"
|
scheduleTypeForm === "dokploy-server" || scheduleTypeForm === "server"
|
||||||
? "max-h-[95vh] sm:max-w-2xl"
|
? "sm:max-w-2xl"
|
||||||
: " sm:max-w-lg",
|
: "sm:max-w-lg",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{scheduleId ? "Edit" : "Create"} Schedule</DialogTitle>
|
<DialogTitle>{scheduleId ? "Edit" : "Create"} Schedule</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{scheduleId ? "Manage" : "Create"} a schedule to run a task at a
|
||||||
|
specific time or interval.
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
import {
|
||||||
|
ClipboardList,
|
||||||
|
Clock,
|
||||||
|
Loader2,
|
||||||
|
Play,
|
||||||
|
Terminal,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -15,15 +24,6 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import {
|
|
||||||
ClipboardList,
|
|
||||||
Clock,
|
|
||||||
Loader2,
|
|
||||||
Play,
|
|
||||||
Terminal,
|
|
||||||
Trash2,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
|
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
|
||||||
import { HandleSchedules } from "./handle-schedules";
|
import { HandleSchedules } from "./handle-schedules";
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={schedule.scheduleId}
|
key={schedule.scheduleId}
|
||||||
className=" flex items-center justify-between rounded-lg border p-3 transition-colors bg-muted/50"
|
className="flex items-center justify-between rounded-lg border p-3 transition-colors bg-muted/50"
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/5">
|
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/5">
|
||||||
@@ -166,12 +166,16 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
|
|
||||||
await runManually({
|
await runManually({
|
||||||
scheduleId: schedule.scheduleId,
|
scheduleId: schedule.scheduleId,
|
||||||
}).then(async () => {
|
})
|
||||||
await new Promise((resolve) =>
|
.then(async () => {
|
||||||
setTimeout(resolve, 1500),
|
await new Promise((resolve) =>
|
||||||
);
|
setTimeout(resolve, 1500),
|
||||||
refetchSchedules();
|
);
|
||||||
});
|
refetchSchedules();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error running schedule");
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Play className="size-4 transition-colors" />
|
<Play className="size-4 transition-colors" />
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { PenBoxIcon } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -20,12 +26,6 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { PenBoxIcon } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const updateApplicationSchema = z.object({
|
const updateApplicationSchema = z.object({
|
||||||
name: z.string().min(1, {
|
name: z.string().min(1, {
|
||||||
@@ -99,7 +99,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
|
|||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Modify Application</DialogTitle>
|
<DialogTitle>Modify Application</DialogTitle>
|
||||||
<DialogDescription>Update the application data</DialogDescription>
|
<DialogDescription>Update the application data</DialogDescription>
|
||||||
|
|||||||
@@ -0,0 +1,696 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import {
|
||||||
|
DatabaseZap,
|
||||||
|
Info,
|
||||||
|
PenBoxIcon,
|
||||||
|
PlusCircle,
|
||||||
|
RefreshCw,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import type { CacheType } from "../domains/handle-domain";
|
||||||
|
import { commonCronExpressions } from "../schedules/handle-schedules";
|
||||||
|
|
||||||
|
const formSchema = z
|
||||||
|
.object({
|
||||||
|
name: z.string().min(1, "Name is required"),
|
||||||
|
cronExpression: z.string().min(1, "Cron expression is required"),
|
||||||
|
volumeName: z.string().min(1, "Volume name is required"),
|
||||||
|
prefix: z.string(),
|
||||||
|
keepLatestCount: z.coerce
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.gte(1, "Must be at least 1")
|
||||||
|
.optional()
|
||||||
|
.nullable(),
|
||||||
|
turnOff: z.boolean().default(false),
|
||||||
|
enabled: z.boolean().default(true),
|
||||||
|
serviceType: z.enum([
|
||||||
|
"application",
|
||||||
|
"compose",
|
||||||
|
"postgres",
|
||||||
|
"mariadb",
|
||||||
|
"mongo",
|
||||||
|
"mysql",
|
||||||
|
"redis",
|
||||||
|
]),
|
||||||
|
serviceName: z.string(),
|
||||||
|
destinationId: z.string().min(1, "Destination required"),
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
if (data.serviceType === "compose" && !data.serviceName) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Service name is required",
|
||||||
|
path: ["serviceName"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.serviceType === "compose" && !data.serviceName) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Service name is required",
|
||||||
|
path: ["serviceName"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id?: string;
|
||||||
|
volumeBackupId?: string;
|
||||||
|
volumeBackupType?:
|
||||||
|
| "application"
|
||||||
|
| "compose"
|
||||||
|
| "postgres"
|
||||||
|
| "mariadb"
|
||||||
|
| "mongo"
|
||||||
|
| "mysql"
|
||||||
|
| "redis";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HandleVolumeBackups = ({
|
||||||
|
id,
|
||||||
|
volumeBackupId,
|
||||||
|
volumeBackupType,
|
||||||
|
}: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
||||||
|
const [keepLatestCountInput, setKeepLatestCountInput] = useState("");
|
||||||
|
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
cronExpression: "",
|
||||||
|
volumeName: "",
|
||||||
|
prefix: "",
|
||||||
|
keepLatestCount: undefined,
|
||||||
|
turnOff: false,
|
||||||
|
enabled: true,
|
||||||
|
serviceName: "",
|
||||||
|
serviceType: volumeBackupType,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const serviceTypeForm = volumeBackupType;
|
||||||
|
const { data: destinations } = api.destination.all.useQuery();
|
||||||
|
const { data: volumeBackup } = api.volumeBackups.one.useQuery(
|
||||||
|
{ volumeBackupId: volumeBackupId || "" },
|
||||||
|
{ enabled: !!volumeBackupId },
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: mounts } = api.mounts.allNamedByApplicationId.useQuery(
|
||||||
|
{ applicationId: id || "" },
|
||||||
|
{ enabled: !!id && volumeBackupType === "application" },
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: services,
|
||||||
|
isFetching: isLoadingServices,
|
||||||
|
error: errorServices,
|
||||||
|
refetch: refetchServices,
|
||||||
|
} = api.compose.loadServices.useQuery(
|
||||||
|
{
|
||||||
|
composeId: id || "",
|
||||||
|
type: cacheType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
retry: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
enabled: !!id && volumeBackupType === "compose",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const serviceName = form.watch("serviceName");
|
||||||
|
|
||||||
|
const { data: mountsByService } = api.compose.loadMountsByService.useQuery(
|
||||||
|
{
|
||||||
|
composeId: id || "",
|
||||||
|
serviceName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!id && volumeBackupType === "compose" && !!serviceName,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (volumeBackupId && volumeBackup) {
|
||||||
|
form.reset({
|
||||||
|
name: volumeBackup.name,
|
||||||
|
cronExpression: volumeBackup.cronExpression,
|
||||||
|
volumeName: volumeBackup.volumeName || "",
|
||||||
|
prefix: volumeBackup.prefix,
|
||||||
|
keepLatestCount: volumeBackup.keepLatestCount || undefined,
|
||||||
|
turnOff: volumeBackup.turnOff,
|
||||||
|
enabled: volumeBackup.enabled || false,
|
||||||
|
serviceName: volumeBackup.serviceName || "",
|
||||||
|
destinationId: volumeBackup.destinationId,
|
||||||
|
serviceType: volumeBackup.serviceType,
|
||||||
|
});
|
||||||
|
setKeepLatestCountInput(
|
||||||
|
volumeBackup.keepLatestCount !== null &&
|
||||||
|
volumeBackup.keepLatestCount !== undefined
|
||||||
|
? String(volumeBackup.keepLatestCount)
|
||||||
|
: "",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [form, volumeBackup, volumeBackupId]);
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = volumeBackupId
|
||||||
|
? api.volumeBackups.update.useMutation()
|
||||||
|
: api.volumeBackups.create.useMutation();
|
||||||
|
|
||||||
|
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||||
|
if (!id && !volumeBackupId) return;
|
||||||
|
|
||||||
|
const preparedKeepLatestCount =
|
||||||
|
keepLatestCountInput === "" ? null : (values.keepLatestCount ?? null);
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
...values,
|
||||||
|
keepLatestCount: preparedKeepLatestCount,
|
||||||
|
destinationId: values.destinationId,
|
||||||
|
volumeBackupId: volumeBackupId || "",
|
||||||
|
serviceType: volumeBackupType,
|
||||||
|
...(volumeBackupType === "application" && {
|
||||||
|
applicationId: id || "",
|
||||||
|
}),
|
||||||
|
...(volumeBackupType === "compose" && {
|
||||||
|
composeId: id || "",
|
||||||
|
}),
|
||||||
|
...(volumeBackupType === "postgres" && {
|
||||||
|
serverId: id || "",
|
||||||
|
}),
|
||||||
|
...(volumeBackupType === "postgres" && {
|
||||||
|
postgresId: id || "",
|
||||||
|
}),
|
||||||
|
...(volumeBackupType === "mariadb" && {
|
||||||
|
mariadbId: id || "",
|
||||||
|
}),
|
||||||
|
...(volumeBackupType === "mongo" && {
|
||||||
|
mongoId: id || "",
|
||||||
|
}),
|
||||||
|
...(volumeBackupType === "mysql" && {
|
||||||
|
mysqlId: id || "",
|
||||||
|
}),
|
||||||
|
...(volumeBackupType === "redis" && {
|
||||||
|
redisId: id || "",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(
|
||||||
|
`Volume backup ${volumeBackupId ? "updated" : "created"} successfully`,
|
||||||
|
);
|
||||||
|
utils.volumeBackups.list.invalidate({
|
||||||
|
id,
|
||||||
|
volumeBackupType,
|
||||||
|
});
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "An unknown error occurred",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
{volumeBackupId ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-blue-500/10"
|
||||||
|
>
|
||||||
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button>
|
||||||
|
<PlusCircle className="w-4 h-4 mr-2" />
|
||||||
|
Add Volume Backup
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent
|
||||||
|
className={cn(
|
||||||
|
volumeBackupType === "compose" || volumeBackupType === "application"
|
||||||
|
? "sm:max-w-2xl"
|
||||||
|
: " sm:max-w-lg",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{volumeBackupId ? "Edit" : "Create"} Volume Backup
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Create a volume backup to backup your volume to a destination
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="flex items-center gap-2">
|
||||||
|
Task Name
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Daily Database Backup" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
A descriptive name for your scheduled task
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="cronExpression"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="flex items-center gap-2">
|
||||||
|
Schedule
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Cron expression format: minute hour day month
|
||||||
|
weekday
|
||||||
|
</p>
|
||||||
|
<p>Example: 0 0 * * * (daily at midnight)</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</FormLabel>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a predefined schedule" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{commonCronExpressions.map((expr) => (
|
||||||
|
<SelectItem key={expr.value} value={expr.value}>
|
||||||
|
{expr.label} ({expr.value})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<div className="relative">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Custom cron expression (e.g., 0 0 * * *)"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FormDescription>
|
||||||
|
Choose a predefined schedule or enter a custom cron
|
||||||
|
expression
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="destinationId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Destination</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a destination" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{destinations?.map((destination) => (
|
||||||
|
<SelectItem
|
||||||
|
key={destination.destinationId}
|
||||||
|
value={destination.destinationId}
|
||||||
|
>
|
||||||
|
{destination.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>
|
||||||
|
Choose the backup destination where files will be stored
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{serviceTypeForm === "compose" && (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col w-full gap-4">
|
||||||
|
{errorServices && (
|
||||||
|
<AlertBlock
|
||||||
|
type="warning"
|
||||||
|
className="[overflow-wrap:anywhere]"
|
||||||
|
>
|
||||||
|
{errorServices?.message}
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="serviceName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Service Name</FormLabel>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value || ""}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a service name" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
{services?.map((service, index) => (
|
||||||
|
<SelectItem
|
||||||
|
value={service}
|
||||||
|
key={`${service}-${index}`}
|
||||||
|
>
|
||||||
|
{service}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectItem value="none" disabled>
|
||||||
|
Empty
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingServices}
|
||||||
|
onClick={() => {
|
||||||
|
if (cacheType === "fetch") {
|
||||||
|
refetchServices();
|
||||||
|
} else {
|
||||||
|
setCacheType("fetch");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Fetch: Will clone the repository and load the
|
||||||
|
services
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingServices}
|
||||||
|
onClick={() => {
|
||||||
|
if (cacheType === "cache") {
|
||||||
|
refetchServices();
|
||||||
|
} else {
|
||||||
|
setCacheType("cache");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DatabaseZap className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Cache: If you previously deployed this
|
||||||
|
compose, it will read the services from the
|
||||||
|
last deployment/fetch from the repository
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{mountsByService && mountsByService.length > 0 && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="volumeName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Volumes</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value || ""}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a volume name" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{mountsByService?.map((volume) => (
|
||||||
|
<SelectItem
|
||||||
|
key={volume.Name}
|
||||||
|
value={volume.Name || ""}
|
||||||
|
>
|
||||||
|
{volume.Name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>
|
||||||
|
Choose the volume to backup, if you dont see the
|
||||||
|
volume here, you can type the volume name manually
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{serviceTypeForm === "application" && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="volumeName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Volumes</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value || ""}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a volume name" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{mounts?.map((mount) => (
|
||||||
|
<SelectItem key={mount.Name} value={mount.Name || ""}>
|
||||||
|
{mount.Name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>
|
||||||
|
Choose the volume to backup, if you dont see the volume
|
||||||
|
here, you can type the volume name manually
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="volumeName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Volume Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="my-volume-name" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
The name of the Docker volume to backup
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="prefix"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Backup Prefix</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="backup-" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Prefix for backup files (optional)
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="keepLatestCount"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Keep Latest Backups</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder="Leave empty to keep all"
|
||||||
|
value={keepLatestCountInput}
|
||||||
|
onChange={(e) => {
|
||||||
|
const raw = e.target.value;
|
||||||
|
setKeepLatestCountInput(raw);
|
||||||
|
if (raw === "") {
|
||||||
|
field.onChange(undefined);
|
||||||
|
} else if (/^\d+$/.test(raw)) {
|
||||||
|
field.onChange(Number(raw));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
How many recent backups to keep. Empty means no cleanup.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="turnOff"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
Turn Off Container During Backup
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription className="text-amber-600 dark:text-amber-400">
|
||||||
|
⚠️ The container will be temporarily stopped during backup to
|
||||||
|
prevent file corruption. This ensures data integrity but may
|
||||||
|
cause temporary service interruption.
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="enabled"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
Enabled
|
||||||
|
</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit" isLoading={isLoading} className="w-full">
|
||||||
|
{volumeBackupId ? "Update" : "Create"} Volume Backup
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,411 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
import { debounce } from "lodash";
|
||||||
|
import { CheckIcon, ChevronsUpDown, Copy, RotateCcw } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { DrawerLogs } from "@/components/shared/drawer-logs";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
} from "@/components/ui/command";
|
||||||
|
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 {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { formatBytes } from "../../database/backups/restore-backup";
|
||||||
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
type: "application" | "compose";
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RestoreBackupSchema = z.object({
|
||||||
|
destinationId: z
|
||||||
|
.string({
|
||||||
|
required_error: "Please select a destination",
|
||||||
|
})
|
||||||
|
.min(1, {
|
||||||
|
message: "Destination is required",
|
||||||
|
}),
|
||||||
|
backupFile: z
|
||||||
|
.string({
|
||||||
|
required_error: "Please select a backup file",
|
||||||
|
})
|
||||||
|
.min(1, {
|
||||||
|
message: "Backup file is required",
|
||||||
|
}),
|
||||||
|
volumeName: z
|
||||||
|
.string({
|
||||||
|
required_error: "Please enter a volume name",
|
||||||
|
})
|
||||||
|
.min(1, {
|
||||||
|
message: "Volume name is required",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
|
||||||
|
|
||||||
|
const { data: destinations = [] } = api.destination.all.useQuery();
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof RestoreBackupSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
destinationId: "",
|
||||||
|
backupFile: "",
|
||||||
|
volumeName: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(RestoreBackupSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const destinationId = form.watch("destinationId");
|
||||||
|
const volumeName = form.watch("volumeName");
|
||||||
|
const backupFile = form.watch("backupFile");
|
||||||
|
|
||||||
|
const debouncedSetSearch = debounce((value: string) => {
|
||||||
|
setDebouncedSearchTerm(value);
|
||||||
|
}, 350);
|
||||||
|
|
||||||
|
const handleSearchChange = (value: string) => {
|
||||||
|
setSearch(value);
|
||||||
|
debouncedSetSearch(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
|
||||||
|
{
|
||||||
|
destinationId: destinationId,
|
||||||
|
search: debouncedSearchTerm,
|
||||||
|
serverId: serverId ?? "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: isOpen && !!destinationId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
|
|
||||||
|
api.volumeBackups.restoreVolumeBackupWithLogs.useSubscription(
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
serviceType: type,
|
||||||
|
serverId,
|
||||||
|
destinationId,
|
||||||
|
volumeName,
|
||||||
|
backupFileName: backupFile,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: isDeploying,
|
||||||
|
onData(log) {
|
||||||
|
if (!isDrawerOpen) {
|
||||||
|
setIsDrawerOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log === "Restore completed successfully!") {
|
||||||
|
setIsDeploying(false);
|
||||||
|
}
|
||||||
|
const parsedLogs = parseLogs(log);
|
||||||
|
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
console.error("Restore logs error:", error);
|
||||||
|
setIsDeploying(false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
setIsDeploying(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline">
|
||||||
|
<RotateCcw className="mr-2 size-4" />
|
||||||
|
Restore Volume Backup
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center">
|
||||||
|
<RotateCcw className="mr-2 size-4" />
|
||||||
|
Restore Volume Backup
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Select a destination and search for volume backup files
|
||||||
|
</DialogDescription>
|
||||||
|
<AlertBlock>
|
||||||
|
Make sure the volume name is not being used by another container.
|
||||||
|
</AlertBlock>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form-restore-backup"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-4"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="destinationId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="">
|
||||||
|
<FormLabel>Destination</FormLabel>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-between !bg-input",
|
||||||
|
!field.value && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{field.value
|
||||||
|
? destinations.find(
|
||||||
|
(d) => d.destinationId === field.value,
|
||||||
|
)?.name
|
||||||
|
: "Select Destination"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Search destinations..."
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
<CommandEmpty>No destinations found.</CommandEmpty>
|
||||||
|
<ScrollArea className="h-64">
|
||||||
|
<CommandGroup>
|
||||||
|
{destinations.map((destination) => (
|
||||||
|
<CommandItem
|
||||||
|
value={destination.destinationId}
|
||||||
|
key={destination.destinationId}
|
||||||
|
onSelect={() => {
|
||||||
|
form.setValue(
|
||||||
|
"destinationId",
|
||||||
|
destination.destinationId,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{destination.name}
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"ml-auto h-4 w-4",
|
||||||
|
destination.destinationId === field.value
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</ScrollArea>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="backupFile"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="">
|
||||||
|
<FormLabel className="flex items-center">
|
||||||
|
Search Backup Files
|
||||||
|
{field.value && (
|
||||||
|
<Badge variant="outline" className="truncate w-52">
|
||||||
|
{field.value}
|
||||||
|
<Copy
|
||||||
|
className="ml-2 size-4 cursor-pointer"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
copy(field.value);
|
||||||
|
toast.success("Backup file copied to clipboard");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<Popover modal>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-between !bg-input",
|
||||||
|
!field.value && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="truncate text-left flex-1 w-52">
|
||||||
|
{field.value || "Search and select a backup file"}
|
||||||
|
</span>
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Search backup files..."
|
||||||
|
value={search}
|
||||||
|
onValueChange={handleSearchChange}
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="py-6 text-center text-sm">
|
||||||
|
Loading backup files...
|
||||||
|
</div>
|
||||||
|
) : files.length === 0 && search ? (
|
||||||
|
<div className="py-6 text-center text-sm text-muted-foreground">
|
||||||
|
No backup files found for "{search}"
|
||||||
|
</div>
|
||||||
|
) : files.length === 0 ? (
|
||||||
|
<div className="py-6 text-center text-sm text-muted-foreground">
|
||||||
|
No backup files available
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ScrollArea className="h-64">
|
||||||
|
<CommandGroup className="w-96">
|
||||||
|
{files?.map((file) => (
|
||||||
|
<CommandItem
|
||||||
|
value={file.Path}
|
||||||
|
key={file.Path}
|
||||||
|
onSelect={() => {
|
||||||
|
form.setValue("backupFile", file.Path);
|
||||||
|
if (file.IsDir) {
|
||||||
|
setSearch(`${file.Path}/`);
|
||||||
|
setDebouncedSearchTerm(`${file.Path}/`);
|
||||||
|
} else {
|
||||||
|
setSearch(file.Path);
|
||||||
|
setDebouncedSearchTerm(file.Path);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex w-full flex-col gap-1">
|
||||||
|
<div className="flex w-full justify-between">
|
||||||
|
<span className="font-medium">
|
||||||
|
{file.Path}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"ml-auto h-4 w-4",
|
||||||
|
file.Path === field.value
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||||
|
<span>
|
||||||
|
Size: {formatBytes(file.Size)}
|
||||||
|
</span>
|
||||||
|
{file.IsDir && (
|
||||||
|
<span className="text-blue-500">
|
||||||
|
Directory
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{file.Hashes?.MD5 && (
|
||||||
|
<span>MD5: {file.Hashes.MD5}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</ScrollArea>
|
||||||
|
)}
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="volumeName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Volume Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Enter volume name" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
isLoading={isDeploying}
|
||||||
|
form="hook-form-restore-backup"
|
||||||
|
type="submit"
|
||||||
|
// disabled={
|
||||||
|
// !form.watch("backupFile") ||
|
||||||
|
// (backupType === "compose" && !form.watch("databaseType"))
|
||||||
|
// }
|
||||||
|
>
|
||||||
|
Restore
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<DrawerLogs
|
||||||
|
isOpen={isDrawerOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsDrawerOpen(false);
|
||||||
|
setFilteredLogs([]);
|
||||||
|
setIsDeploying(false);
|
||||||
|
// refetch();
|
||||||
|
}}
|
||||||
|
filteredLogs={filteredLogs}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
import {
|
||||||
|
ClipboardList,
|
||||||
|
DatabaseBackup,
|
||||||
|
Loader2,
|
||||||
|
Play,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
|
||||||
|
import { HandleVolumeBackups } from "./handle-volume-backups";
|
||||||
|
import { RestoreVolumeBackups } from "./restore-volume-backups";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
type?: "application" | "compose";
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowVolumeBackups = ({
|
||||||
|
id,
|
||||||
|
type = "application",
|
||||||
|
serverId,
|
||||||
|
}: Props) => {
|
||||||
|
const {
|
||||||
|
data: volumeBackups,
|
||||||
|
isLoading: isLoadingVolumeBackups,
|
||||||
|
refetch: refetchVolumeBackups,
|
||||||
|
} = api.volumeBackups.list.useQuery(
|
||||||
|
{
|
||||||
|
id: id || "",
|
||||||
|
volumeBackupType: type,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const utils = api.useUtils();
|
||||||
|
|
||||||
|
const { mutateAsync: deleteVolumeBackup, isLoading: isDeleting } =
|
||||||
|
api.volumeBackups.delete.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: runManually, isLoading } =
|
||||||
|
api.volumeBackups.runManually.useMutation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
|
||||||
|
<CardHeader className="px-0">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<CardTitle className="text-xl font-bold flex items-center gap-2">
|
||||||
|
Volume Backups
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Schedule volume backups to run automatically at specified
|
||||||
|
intervals.
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{volumeBackups && volumeBackups.length > 0 && (
|
||||||
|
<>
|
||||||
|
<HandleVolumeBackups id={id} volumeBackupType={type} />
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RestoreVolumeBackups
|
||||||
|
id={id}
|
||||||
|
type={type}
|
||||||
|
serverId={serverId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="px-0">
|
||||||
|
{isLoadingVolumeBackups ? (
|
||||||
|
<div className="flex gap-4 w-full items-center justify-center text-center mx-auto min-h-[45vh]">
|
||||||
|
<Loader2 className="size-4 text-muted-foreground/70 transition-colors animate-spin self-center" />
|
||||||
|
<span className="text-sm text-muted-foreground/70">
|
||||||
|
Loading volume backups...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : volumeBackups && volumeBackups.length > 0 ? (
|
||||||
|
<div className="grid xl:grid-cols-2 gap-4 grid-cols-1 h-full">
|
||||||
|
{volumeBackups.map((volumeBackup) => {
|
||||||
|
const serverId =
|
||||||
|
volumeBackup.application?.serverId ||
|
||||||
|
volumeBackup.postgres?.serverId ||
|
||||||
|
volumeBackup.mysql?.serverId ||
|
||||||
|
volumeBackup.mariadb?.serverId ||
|
||||||
|
volumeBackup.mongo?.serverId ||
|
||||||
|
volumeBackup.redis?.serverId ||
|
||||||
|
volumeBackup.compose?.serverId;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={volumeBackup.volumeBackupId}
|
||||||
|
className="flex items-center justify-between rounded-lg border p-3 transition-colors bg-muted/50"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/5">
|
||||||
|
<DatabaseBackup className="size-4 text-primary/70" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="text-sm font-medium leading-none">
|
||||||
|
{volumeBackup.name}
|
||||||
|
</h3>
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
volumeBackup.enabled ? "default" : "secondary"
|
||||||
|
}
|
||||||
|
className="text-[10px] px-1 py-0"
|
||||||
|
>
|
||||||
|
{volumeBackup.enabled ? "Enabled" : "Disabled"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="font-mono text-[10px] bg-transparent"
|
||||||
|
>
|
||||||
|
Cron: {volumeBackup.cronExpression}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<ShowDeploymentsModal
|
||||||
|
id={volumeBackup.volumeBackupId}
|
||||||
|
type="volumeBackup"
|
||||||
|
serverId={serverId || undefined}
|
||||||
|
>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<ClipboardList className="size-4 transition-colors " />
|
||||||
|
</Button>
|
||||||
|
</ShowDeploymentsModal>
|
||||||
|
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
isLoading={isLoading}
|
||||||
|
onClick={async () => {
|
||||||
|
toast.success("Volume backup run successfully");
|
||||||
|
|
||||||
|
await runManually({
|
||||||
|
volumeBackupId: volumeBackup.volumeBackupId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, 1500),
|
||||||
|
);
|
||||||
|
refetchVolumeBackups();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error running volume backup");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Play className="size-4 transition-colors" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
Run Manual Volume Backup
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<HandleVolumeBackups
|
||||||
|
volumeBackupId={volumeBackup.volumeBackupId}
|
||||||
|
id={id}
|
||||||
|
volumeBackupType={type}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogAction
|
||||||
|
title="Delete Volume Backup"
|
||||||
|
description="Are you sure you want to delete this volume backup?"
|
||||||
|
type="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
await deleteVolumeBackup({
|
||||||
|
volumeBackupId: volumeBackup.volumeBackupId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
utils.volumeBackups.list.invalidate({
|
||||||
|
id,
|
||||||
|
volumeBackupType: type,
|
||||||
|
});
|
||||||
|
toast.success("Volume backup deleted successfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deleting volume backup");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10 "
|
||||||
|
isLoading={isDeleting}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-2 items-center justify-center py-12 rounded-lg">
|
||||||
|
<DatabaseBackup className="size-8 mb-4 text-muted-foreground" />
|
||||||
|
<p className="text-lg font-medium text-muted-foreground">
|
||||||
|
No volume backups
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Create your first volume backup to automate your workflows
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HandleVolumeBackups id={id} volumeBackupType={type} />
|
||||||
|
<RestoreVolumeBackups id={id} type={type} serverId={serverId} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -18,11 +23,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
interface Props {
|
interface Props {
|
||||||
composeId: string;
|
composeId: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { AlertTriangle, Loader2 } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schema for Isolated Deployment
|
||||||
|
const isolatedSchema = z.object({
|
||||||
|
isolatedDeployment: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type IsolatedSchema = z.infer<typeof isolatedSchema>;
|
||||||
|
|
||||||
|
export const IsolatedDeploymentTab = ({ composeId }: Props) => {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const [compose, setCompose] = useState<string>("");
|
||||||
|
const [isPreviewLoading, setIsPreviewLoading] = useState<boolean>(false);
|
||||||
|
const { mutateAsync, error, isError } =
|
||||||
|
api.compose.isolatedDeployment.useMutation();
|
||||||
|
|
||||||
|
const [isOpenPreview, setIsOpenPreview] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const { mutateAsync: updateCompose } = api.compose.update.useMutation();
|
||||||
|
|
||||||
|
const { data, refetch } = api.compose.one.useQuery(
|
||||||
|
{ composeId },
|
||||||
|
{ enabled: !!composeId },
|
||||||
|
);
|
||||||
|
|
||||||
|
const form = useForm<IsolatedSchema>({
|
||||||
|
defaultValues: {
|
||||||
|
isolatedDeployment: false,
|
||||||
|
},
|
||||||
|
resolver: zodResolver(isolatedSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
isolatedDeployment: data?.isolatedDeployment || false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||||
|
|
||||||
|
const onSubmit = async (formData: IsolatedSchema) => {
|
||||||
|
await updateCompose({
|
||||||
|
composeId,
|
||||||
|
isolatedDeployment: formData?.isolatedDeployment || false,
|
||||||
|
})
|
||||||
|
.then(async (_data) => {
|
||||||
|
await refetch();
|
||||||
|
toast.success("Compose updated");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error updating the compose");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const generatePreview = async () => {
|
||||||
|
setIsOpenPreview(true);
|
||||||
|
setIsPreviewLoading(true);
|
||||||
|
try {
|
||||||
|
await mutateAsync({
|
||||||
|
composeId,
|
||||||
|
suffix: data?.appName || "",
|
||||||
|
}).then(async (data) => {
|
||||||
|
await utils.project.all.invalidate();
|
||||||
|
setCompose(data);
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
toast.error("Error generating preview");
|
||||||
|
setIsOpenPreview(false);
|
||||||
|
} finally {
|
||||||
|
setIsPreviewLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl">Enable Isolated Deployment</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Configure isolated deployment to the compose file.
|
||||||
|
<div className="text-sm text-muted-foreground flex flex-col gap-2">
|
||||||
|
<span>
|
||||||
|
This feature creates an isolated environment for your deployment
|
||||||
|
by adding unique prefixes to all resources. It establishes a
|
||||||
|
dedicated network based on your compose file's name, ensuring your
|
||||||
|
services run in isolation. This prevents conflicts when running
|
||||||
|
multiple instances of the same template or services with identical
|
||||||
|
names.
|
||||||
|
</span>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2">
|
||||||
|
Resources that will be isolated:
|
||||||
|
</h4>
|
||||||
|
<ul className="list-disc list-inside">
|
||||||
|
<li>Docker networks</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
id="isolated-deployment-form"
|
||||||
|
className="grid w-full gap-4"
|
||||||
|
>
|
||||||
|
{isError && (
|
||||||
|
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
|
||||||
|
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||||
|
<span className="text-sm text-red-600 dark:text-red-400">
|
||||||
|
{error?.message}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col lg:flex-col gap-4 w-full">
|
||||||
|
<div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="isolatedDeployment"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>
|
||||||
|
Enable Isolated Deployment ({data?.appName})
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Enable isolated deployment to the compose file.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
|
||||||
|
<Button
|
||||||
|
form="isolated-deployment-form"
|
||||||
|
type="submit"
|
||||||
|
className="lg:w-fit"
|
||||||
|
isLoading={form.formState.isSubmitting}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
|
||||||
|
<Button
|
||||||
|
onClick={generatePreview}
|
||||||
|
isLoading={isPreviewLoading}
|
||||||
|
variant="secondary"
|
||||||
|
className="lg:w-fit"
|
||||||
|
>
|
||||||
|
Preview Compose
|
||||||
|
</Button>
|
||||||
|
<Dialog open={isOpenPreview} onOpenChange={setIsOpenPreview}>
|
||||||
|
<DialogContent className="sm:max-w-6xl max-h-[80vh]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Isolated Deployment Preview</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Preview of the compose file with isolated deployment
|
||||||
|
configuration
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex flex-col gap-4 overflow-auto">
|
||||||
|
{isPreviewLoading ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 gap-4">
|
||||||
|
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Generating compose preview...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<pre>
|
||||||
|
<CodeEditor
|
||||||
|
value={compose || ""}
|
||||||
|
language="yaml"
|
||||||
|
readOnly
|
||||||
|
height="60vh"
|
||||||
|
/>
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,3 +1,13 @@
|
|||||||
|
import type { ServiceType } from "@dokploy/server/db/schema";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
import { Copy, Trash2 } from "lucide-react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
@@ -20,15 +30,6 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import type { ServiceType } from "@dokploy/server/db/schema";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import copy from "copy-to-clipboard";
|
|
||||||
import { Copy, Trash2 } from "lucide-react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const deleteComposeSchema = z.object({
|
const deleteComposeSchema = z.object({
|
||||||
projectName: z.string().min(1, {
|
projectName: z.string().min(1, {
|
||||||
@@ -114,6 +115,12 @@ export const DeleteService = ({ id, type }: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isDisabled =
|
||||||
|
(data &&
|
||||||
|
"applicationStatus" in data &&
|
||||||
|
data?.applicationStatus === "running") ||
|
||||||
|
(data && "composeStatus" in data && data?.composeStatus === "running");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -126,7 +133,7 @@ export const DeleteService = ({ id, type }: Props) => {
|
|||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -202,6 +209,12 @@ export const DeleteService = ({ id, type }: Props) => {
|
|||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
{isDisabled && (
|
||||||
|
<AlertBlock type="warning" className="w-full mt-5">
|
||||||
|
Cannot delete the service while it is running. Please wait for the
|
||||||
|
build to finish and then try again.
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -211,8 +224,10 @@ export const DeleteService = ({ id, type }: Props) => {
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
disabled={isDisabled}
|
||||||
form="hook-form-delete-compose"
|
form="hook-form-delete-compose"
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
|
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
@@ -8,10 +12,6 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
||||||
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user