mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-29 11:05:33 +02:00
Compare commits
1037 Commits
feat/caddy
...
v0.25.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f5c2dbe92 | ||
|
|
0f9505327f | ||
|
|
dd2902a57c | ||
|
|
0138a7c011 | ||
|
|
845d2a3ac5 | ||
|
|
4033bb84b2 | ||
|
|
43e96edcdd | ||
|
|
2db388536f | ||
|
|
43876efc79 | ||
|
|
e7c7545c02 | ||
|
|
77705381cd | ||
|
|
5fdf82a27f | ||
|
|
6bd5b1f71f | ||
|
|
17d6830b66 | ||
|
|
a845eba320 | ||
|
|
2f4ec9f35f | ||
|
|
b725861b55 | ||
|
|
6fa8f63277 | ||
|
|
ac6bdf60ec | ||
|
|
db292e6949 | ||
|
|
085f6bbbb7 | ||
|
|
cbdc4e4a20 | ||
|
|
ee3ff18feb | ||
|
|
f5084dd5fb | ||
|
|
1b603d84d7 | ||
|
|
cf2c89d136 | ||
|
|
95de98e94d | ||
|
|
569d43ae7f | ||
|
|
d22ed9b569 | ||
|
|
8b88c85b37 | ||
|
|
11fbd047d0 | ||
|
|
69af9c0312 | ||
|
|
063d51e442 | ||
|
|
0a789e1d6f | ||
|
|
671cd497fd | ||
|
|
8ddc254252 | ||
|
|
2668e22302 | ||
|
|
37145fbdf2 | ||
|
|
6847d8dbef | ||
|
|
032bcb7459 | ||
|
|
68be7a259f | ||
|
|
7d682870ff | ||
|
|
d1a1a80c77 | ||
|
|
3d7dc82232 | ||
|
|
fedc88eb40 | ||
|
|
5d0f6a4657 | ||
|
|
4718461405 | ||
|
|
80b22d9458 | ||
|
|
8fa5fe7f2c | ||
|
|
4ced8bec96 | ||
|
|
9ecb770a01 | ||
|
|
8ac586b2f7 | ||
|
|
0a1800ba6d | ||
|
|
f13028ee70 | ||
|
|
b6b6b9f2ce | ||
|
|
f46637b8e1 | ||
|
|
948ed2cc0d | ||
|
|
a536c977f0 | ||
|
|
8524cd0972 | ||
|
|
ac1e51cd11 | ||
|
|
ca243d7259 | ||
|
|
e1ce54c159 | ||
|
|
031302d808 | ||
|
|
5e01505e4d | ||
|
|
c423724972 | ||
|
|
f1f7639708 | ||
|
|
9ef1a76a85 | ||
|
|
30b66a4828 | ||
|
|
4416ca9cd2 | ||
|
|
f2ead66890 | ||
|
|
64475bbb13 | ||
|
|
c1896f8877 | ||
|
|
d13975adac | ||
|
|
d9398b9558 | ||
|
|
788dbe4050 | ||
|
|
6934f44778 | ||
|
|
b8e9602538 | ||
|
|
afca968853 | ||
|
|
457a6db00f | ||
|
|
81f89a0796 | ||
|
|
d8a98f3936 | ||
|
|
c9715b19a3 | ||
|
|
ec11325165 | ||
|
|
abcbd2d599 | ||
|
|
1664ae9b92 | ||
|
|
24729f35ec | ||
|
|
3eaeaa1db4 | ||
|
|
de4a00f1e9 | ||
|
|
2f5cd620c5 | ||
|
|
1763000070 | ||
|
|
3519913886 | ||
|
|
63e578f13c | ||
|
|
d80ada7c00 | ||
|
|
766cd20e90 | ||
|
|
4e69c70697 | ||
|
|
3b7d009841 | ||
|
|
b4e29dab39 | ||
|
|
090ec2b3b9 | ||
|
|
57dc24bcb1 | ||
|
|
f630b889c6 | ||
|
|
a2abb205fd | ||
|
|
1f2dabb16b | ||
|
|
ffb69fedff | ||
|
|
fbc087bd84 | ||
|
|
ccb995cb7d | ||
|
|
02685fde9d | ||
|
|
fc2bd44983 | ||
|
|
30a2d78a5b | ||
|
|
081ba60f6e | ||
|
|
2d41db7f37 | ||
|
|
d0f54f2067 | ||
|
|
a6ca41f91f | ||
|
|
b2b649c5cd | ||
|
|
225c398d31 | ||
|
|
07b99bd4e4 | ||
|
|
652e8910f4 | ||
|
|
e04e25385d | ||
|
|
6833713697 | ||
|
|
d0489f6e11 | ||
|
|
39872720dd | ||
|
|
b90f0135d4 | ||
|
|
35fc04dc8f | ||
|
|
c6509efa65 | ||
|
|
3891798b17 | ||
|
|
3662c1a684 | ||
|
|
d96e9071f2 | ||
|
|
e637a4ad99 | ||
|
|
1ce15da7ce | ||
|
|
0dca1b2216 | ||
|
|
c73a14a379 | ||
|
|
392e3434c4 | ||
|
|
e3f3426f1c | ||
|
|
a09cd06eea | ||
|
|
87a41ca710 | ||
|
|
35b7b5bd68 | ||
|
|
16c37c3ceb | ||
|
|
42548f310e | ||
|
|
47b66d0dc3 | ||
|
|
32cbc5b4b7 | ||
|
|
15171622df | ||
|
|
46f1af3bb3 | ||
|
|
d199a54033 | ||
|
|
fb749cd862 | ||
|
|
4c5771b55b | ||
|
|
7e1de62ab1 | ||
|
|
d67644e52f | ||
|
|
52e21dab4e | ||
|
|
4a3a7fa47b | ||
|
|
68945c6888 | ||
|
|
146d82b6c4 | ||
|
|
02215d4e21 | ||
|
|
4ca05414af | ||
|
|
aa7e382818 | ||
|
|
87a9ed46ba | ||
|
|
90d9880301 | ||
|
|
940b9967b8 | ||
|
|
741085466b | ||
|
|
11b0e21728 | ||
|
|
990b174110 | ||
|
|
4c4c72bc9c | ||
|
|
8f446d04f3 | ||
|
|
e8a5f9c0a8 | ||
|
|
c57c231c32 | ||
|
|
8194929558 | ||
|
|
4a07118acd | ||
|
|
be9e19e708 | ||
|
|
3e7eff11cd | ||
|
|
f8ebf77575 | ||
|
|
de3c845ab0 | ||
|
|
cb992259cf | ||
|
|
883c3f9739 | ||
|
|
766890192d | ||
|
|
1a9f131d39 | ||
|
|
59cbc8ee0d | ||
|
|
e9322fc900 | ||
|
|
39d48d8bdf | ||
|
|
399bcb0302 | ||
|
|
e0b6a8627a | ||
|
|
ecf7ae924f | ||
|
|
d57a0cf439 | ||
|
|
52d2bd2114 | ||
|
|
72f8a28f4f | ||
|
|
6fc325fe95 | ||
|
|
fd199fdcc0 | ||
|
|
5e1a164a54 | ||
|
|
bc2b4f1369 | ||
|
|
38abe03257 | ||
|
|
22e40134ea | ||
|
|
a2841fdd30 | ||
|
|
468feaa092 | ||
|
|
caf244120c | ||
|
|
7273c636a0 | ||
|
|
b9a8b27441 | ||
|
|
9f1f13b21b | ||
|
|
793a8ba760 | ||
|
|
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 | ||
|
|
d52fe5c050 | ||
|
|
36281cd5d3 | ||
|
|
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 | ||
|
|
892f272108 | ||
|
|
fca537ee40 | ||
|
|
ae24aa8be5 | ||
|
|
b74d3995ee | ||
|
|
f7fd77f7e9 | ||
|
|
db8a4e6edf | ||
|
|
fa16cfec2a | ||
|
|
f35d084dd4 | ||
|
|
274daf52c0 | ||
|
|
da52d767eb | ||
|
|
45a178e705 | ||
|
|
ebf9db7cc0 | ||
|
|
8599f519a4 | ||
|
|
113e4ae4b5 | ||
|
|
7f0bdc7e00 | ||
|
|
b685a817fd | ||
|
|
6061a443d1 | ||
|
|
4c9835d1f3 | ||
|
|
ec6c685a28 | ||
|
|
7b14e4c5d2 | ||
|
|
316f592e09 | ||
|
|
bd82199ae0 | ||
|
|
89d573a2f5 | ||
|
|
3d285ca437 | ||
|
|
8c5e34c528 | ||
|
|
98199e65bf | ||
|
|
bf1026af7a | ||
|
|
7c9767d90f | ||
|
|
688f6478f1 | ||
|
|
cad17e0f7f | ||
|
|
d97461d820 | ||
|
|
9686848090 | ||
|
|
a7b644e403 | ||
|
|
96b4c334da | ||
|
|
1b99c3ac23 | ||
|
|
a12b514525 | ||
|
|
ea91b01461 | ||
|
|
149b8f70d8 | ||
|
|
6be4984649 | ||
|
|
7ec68e688b | ||
|
|
b30f8944c4 | ||
|
|
f0d242b9b9 | ||
|
|
b6d86b4732 | ||
|
|
304134cdda | ||
|
|
c84b271511 | ||
|
|
f2671f9369 | ||
|
|
bb904bb011 | ||
|
|
96dd8d37a5 | ||
|
|
be91b53c86 | ||
|
|
98c77d539e | ||
|
|
67f5befa48 | ||
|
|
5b2056101f | ||
|
|
000b4ba49e | ||
|
|
4efa56aae5 | ||
|
|
a788a73fa3 | ||
|
|
319ca6944d | ||
|
|
238736db8d | ||
|
|
9fb6ca2b3b | ||
|
|
9f146d7d80 | ||
|
|
556a437251 | ||
|
|
ef5e1d6818 | ||
|
|
1089a8247d | ||
|
|
ef0cef99a1 | ||
|
|
8737dc86c9 | ||
|
|
cf06e5369a | ||
|
|
973de2a610 | ||
|
|
f8baf6fe41 | ||
|
|
3e05be4513 | ||
|
|
b3b009761a | ||
|
|
a659594134 | ||
|
|
9a1f0b467d | ||
|
|
e8b3abb7c9 | ||
|
|
8215d2e79f | ||
|
|
9c19b1efa3 | ||
|
|
4966bbeb73 | ||
|
|
df97dc0179 | ||
|
|
b14b9300c0 | ||
|
|
a7d1fabd81 | ||
|
|
d171e3da91 | ||
|
|
2c77029dad | ||
|
|
030e482fce | ||
|
|
e53c67f0d9 | ||
|
|
0c12d967e2 | ||
|
|
98aabd7bd8 | ||
|
|
88e862544b | ||
|
|
7f9c19bc11 | ||
|
|
9535276fe6 | ||
|
|
56d21aff60 | ||
|
|
8436d364be | ||
|
|
5d5e56d144 | ||
|
|
0627b6fd3a | ||
|
|
39af44daef | ||
|
|
2619cb49d1 | ||
|
|
46d12fa9d8 | ||
|
|
51ee46496c | ||
|
|
a13e24dab0 | ||
|
|
4aac3476b6 | ||
|
|
037343a796 | ||
|
|
274d80ea7c | ||
|
|
629889f1a8 | ||
|
|
3e74ce05a7 | ||
|
|
d05218e848 | ||
|
|
0fbad4f75e | ||
|
|
c3cbaf2a57 | ||
|
|
560d493d56 | ||
|
|
27b2106630 | ||
|
|
609954c366 | ||
|
|
84faa9747e | ||
|
|
4b370ef43e | ||
|
|
b94a6bff92 | ||
|
|
276b754377 | ||
|
|
f3b3798362 | ||
|
|
461acc354e | ||
|
|
dfc75a9116 | ||
|
|
e1580bad23 | ||
|
|
b567ec1d83 | ||
|
|
9c73b8dc36 | ||
|
|
7348526873 | ||
|
|
6fc83f2db3 | ||
|
|
43d22c2bd4 | ||
|
|
38a5313967 | ||
|
|
ba3645933f | ||
|
|
2fa2e76e2e | ||
|
|
17a26353b6 | ||
|
|
e2c163c6d5 | ||
|
|
616e11722c | ||
|
|
91a44706df | ||
|
|
748de47a6d | ||
|
|
cbf9aef0df | ||
|
|
e2befc24a5 | ||
|
|
0f48f2c830 | ||
|
|
5dfa7645f3 | ||
|
|
7fe163dd33 | ||
|
|
19b56771b8 | ||
|
|
cff01ed438 | ||
|
|
10fa3c8cf1 | ||
|
|
6c5497ed21 | ||
|
|
380656efee | ||
|
|
c64d2245ce | ||
|
|
a985998b93 | ||
|
|
4f3ba16dfa | ||
|
|
6c788429f1 | ||
|
|
3176a9d7e3 | ||
|
|
94a6a9587e | ||
|
|
911681f389 | ||
|
|
5992688e85 | ||
|
|
425061e481 | ||
|
|
08c0bf8a21 | ||
|
|
64a2c9e0a1 | ||
|
|
21e46f5382 | ||
|
|
52b2158309 | ||
|
|
178d84d438 | ||
|
|
80016b57a8 | ||
|
|
b4b2d12f6e | ||
|
|
294378d95b | ||
|
|
c52812f9d3 | ||
|
|
82f7c5d5f3 | ||
|
|
3d2ae52259 | ||
|
|
bf115c7895 | ||
|
|
c2c29dbaba | ||
|
|
d4032f34bf | ||
|
|
136570b36c | ||
|
|
7d0075c230 | ||
|
|
19b4edee8d | ||
|
|
7f04eb856e | ||
|
|
5156b45ffc | ||
|
|
80e6f21840 | ||
|
|
5b519151e8 | ||
|
|
2ad8bf355b | ||
|
|
aa475e6123 | ||
|
|
66756c34fe | ||
|
|
946a5739dc | ||
|
|
6c817a9e5d | ||
|
|
6aea937e86 | ||
|
|
cad628d155 | ||
|
|
19612d4b66 | ||
|
|
47dd003461 | ||
|
|
def99225fc | ||
|
|
32405fc61a | ||
|
|
25e1a9af57 | ||
|
|
1fcb1f2c5e | ||
|
|
fdaba7e752 | ||
|
|
c1640cba29 | ||
|
|
3bd54ff61e | ||
|
|
5853d18bc1 | ||
|
|
f575317906 | ||
|
|
e6028e73ac | ||
|
|
bcbed151e8 | ||
|
|
c708f7ba62 | ||
|
|
95a538f261 | ||
|
|
f854457d69 | ||
|
|
cd998c37f1 | ||
|
|
d46a61098b | ||
|
|
8f14d854a0 | ||
|
|
388399b370 | ||
|
|
a8b4bb9c41 | ||
|
|
ebc8c2f73d | ||
|
|
1227d2b5fc | ||
|
|
314438b84c | ||
|
|
cc5574e08a | ||
|
|
11a8fcc476 | ||
|
|
c50229a33c | ||
|
|
0609d74d2b | ||
|
|
fce8eca894 | ||
|
|
3de0d674ed | ||
|
|
7faab54a65 | ||
|
|
40d9db7ccf | ||
|
|
c7c01f57d4 | ||
|
|
45cf295be0 | ||
|
|
79372527e6 | ||
|
|
edcfc7d670 | ||
|
|
6277ebaaec | ||
|
|
2b081166f9 | ||
|
|
d8f12f1780 | ||
|
|
95d949f112 | ||
|
|
1ec0c8e8b3 | ||
|
|
b9ac25ef42 | ||
|
|
9fe2460b88 | ||
|
|
44af0ec975 | ||
|
|
b84b4549a0 | ||
|
|
c110fae965 | ||
|
|
86b56e2597 | ||
|
|
7e365e1947 | ||
|
|
d458536803 | ||
|
|
9cb5b9a7d0 | ||
|
|
7e9fccfcb0 | ||
|
|
1c73dab719 | ||
|
|
3ec339fc89 | ||
|
|
c13a68dab4 | ||
|
|
eb5ba2f219 | ||
|
|
1eda6513df | ||
|
|
4bb60b9f7e | ||
|
|
9948dd7f19 | ||
|
|
e3ec8f1589 | ||
|
|
9aa56870b0 | ||
|
|
06c9e43143 | ||
|
|
e09447d4b4 | ||
|
|
2fa0c7dfd2 | ||
|
|
27521decbd | ||
|
|
4bec311ad0 | ||
|
|
432f616896 | ||
|
|
4575f16be4 | ||
|
|
d6f5f6e6cb | ||
|
|
96b1df2199 | ||
|
|
614b9d25a8 | ||
|
|
66dd890448 | ||
|
|
557c89ac6d | ||
|
|
b3e2af3b40 | ||
|
|
a8159e5f99 | ||
|
|
89306a7619 | ||
|
|
0690f07262 | ||
|
|
e437903ef8 | ||
|
|
50aeeb2fb8 | ||
|
|
d85fc2e513 | ||
|
|
cfba9a7d79 | ||
|
|
4a8cadc6ee | ||
|
|
b2d938d2fc | ||
|
|
9277427153 | ||
|
|
ccb141339b | ||
|
|
c87af312ca | ||
|
|
a5fb5532fd | ||
|
|
43ab1aa7b8 | ||
|
|
3072795232 | ||
|
|
98d0f1d5bf | ||
|
|
49e55961db | ||
|
|
4ee220c1d8 | ||
|
|
c69992c4f0 | ||
|
|
1f6ba45c12 | ||
|
|
ef02ba22b5 | ||
|
|
d7daa6d8e0 | ||
|
|
fafa14c10a | ||
|
|
2c90103823 | ||
|
|
f2bb01c800 | ||
|
|
442f051457 | ||
|
|
e84ce38994 | ||
|
|
0ea264ea42 | ||
|
|
d4064805eb | ||
|
|
d5c77fded3 | ||
|
|
d853b1d326 | ||
|
|
d57e347fdc | ||
|
|
25f3980492 | ||
|
|
c8e2f4bfdc | ||
|
|
4afc6ac250 | ||
|
|
52a660add3 | ||
|
|
3ad5982f39 | ||
|
|
8ba4ac22cc | ||
|
|
bcebcfdfdf | ||
|
|
77b1ec4733 | ||
|
|
cfae5f7e6c | ||
|
|
0f67e9e222 | ||
|
|
c4045795ee | ||
|
|
24f3be3c00 | ||
|
|
5055994bd3 | ||
|
|
ddcb22dff9 | ||
|
|
77d7dc1f22 | ||
|
|
19bf4f27b6 | ||
|
|
6b9765a26c | ||
|
|
8d91053c3a | ||
|
|
7c2eb63625 | ||
|
|
2ea2605ab1 | ||
|
|
7ae3ff22ee | ||
|
|
fa6baa0c1a | ||
|
|
5b43df92c1 | ||
|
|
f3032bc94f | ||
|
|
eef874ecd4 | ||
|
|
d6daa5677a | ||
|
|
dc03ba73b3 | ||
|
|
5c2159f7b2 | ||
|
|
ffcdbcf046 | ||
|
|
bb5c6bebff | ||
|
|
144d48057c | ||
|
|
55dc08b6ba | ||
|
|
56f525803b | ||
|
|
91bcd1238f | ||
|
|
120646c77b | ||
|
|
c0b35efaca | ||
|
|
22dee88e51 | ||
|
|
1645f7e932 | ||
|
|
b4aeb6577e | ||
|
|
fdd330ca19 | ||
|
|
33de620893 | ||
|
|
6518407c0c | ||
|
|
6f47999a2e | ||
|
|
fe69d5d405 | ||
|
|
a6880fd38c | ||
|
|
5d25de13dd | ||
|
|
5611dcccfd | ||
|
|
e2a1882fe3 | ||
|
|
ceb16ae9f7 | ||
|
|
1911b5b674 | ||
|
|
6b818bbb51 | ||
|
|
79796185d6 | ||
|
|
461d7c530a | ||
|
|
ade4b8dd1b | ||
|
|
f49a67f8df | ||
|
|
c3986d7a08 | ||
|
|
0bf4e5560c | ||
|
|
79d55d8d34 | ||
|
|
d4c6e5b048 | ||
|
|
cd4eed3507 | ||
|
|
a650bd16fb | ||
|
|
4e5b5f219e | ||
|
|
dfda934726 | ||
|
|
e6d0b7b4ee | ||
|
|
d0dbc1837f | ||
|
|
2b5af1897f | ||
|
|
11b9cee73d | ||
|
|
bc17991580 | ||
|
|
459c94929a | ||
|
|
8d28a50a17 | ||
|
|
08bbeceeba | ||
|
|
b7bf09bf21 | ||
|
|
546c6ade82 | ||
|
|
db2e3691a5 | ||
|
|
a6dca144a8 | ||
|
|
43a17e7e75 | ||
|
|
da60c4f3a8 | ||
|
|
764f8ec993 | ||
|
|
8c36e48fe7 | ||
|
|
ef7918a33a | ||
|
|
af4511040f | ||
|
|
4b15177260 | ||
|
|
cd8230b0e5 | ||
|
|
f5cffca37c | ||
|
|
e5ee06b67e | ||
|
|
1bbbdfba60 | ||
|
|
fcb8a2bded | ||
|
|
116e33ce37 | ||
|
|
e9b92d2641 | ||
|
|
96e9799afb | ||
|
|
3e07be38df | ||
|
|
67e85cabcb | ||
|
|
64a77decfd | ||
|
|
cc5a3e6873 | ||
|
|
b1d1763988 | ||
|
|
b5d199057d | ||
|
|
bfb6baf572 | ||
|
|
371c6317aa | ||
|
|
1f81794904 | ||
|
|
d5d3831d54 | ||
|
|
856399550a | ||
|
|
86b8b0987b | ||
|
|
0dac1fefe6 | ||
|
|
633ba899e0 |
18
.github/pull_request_template.md
vendored
Normal file
18
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
## 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)
|
||||||
|
|
||||||
|
closes #123
|
||||||
|
|
||||||
|
## Screenshots (if applicable)
|
||||||
|
|
||||||
BIN
.github/sponsors/agentdock.png
vendored
Normal file
BIN
.github/sponsors/agentdock.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
.github/sponsors/american-cloud.png
vendored
Normal file
BIN
.github/sponsors/american-cloud.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
.github/sponsors/tuple.png
vendored
Normal file
BIN
.github/sponsors/tuple.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 264 KiB |
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
|
||||||
|
|||||||
41
.github/workflows/pull-request.yml
vendored
41
.github/workflows/pull-request.yml
vendored
@@ -4,43 +4,22 @@ 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
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.9.0
|
node-version: 20.16.0
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm run server:build
|
- run: pnpm 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.9.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.9.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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,7 +52,7 @@ feat: add new feature
|
|||||||
|
|
||||||
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
|
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
|
||||||
|
|
||||||
We use Node v20.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory.
|
We use Node v20.16.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.16.0 && nvm use` in the root directory.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/dokploy/dokploy.git
|
git clone https://github.com/dokploy/dokploy.git
|
||||||
@@ -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
|
||||||
@@ -147,14 +148,12 @@ curl -sSL https://railpack.com/install.sh | sh
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install Buildpacks
|
# Install Buildpacks
|
||||||
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.35.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 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.
|
||||||
@@ -163,13 +162,18 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/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
|
||||||
|
|
||||||
To add a new template, go to `https://github.com/Dokploy/templates` repository and read the README.md file.
|
To add a new template, go to `https://github.com/Dokploy/templates` repository and read the README.md file.
|
||||||
|
|
||||||
|
|
||||||
### Recommendations
|
### Recommendations
|
||||||
|
|
||||||
- Use the same name of the folder as the id of the template.
|
- Use the same name of the folder as the id of the template.
|
||||||
|
|||||||
10
Dockerfile
10
Dockerfile
@@ -1,7 +1,9 @@
|
|||||||
FROM node:20.9-slim AS base
|
# syntax=docker/dockerfile:1
|
||||||
|
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
|
||||||
@@ -29,7 +31,7 @@ WORKDIR /app
|
|||||||
# Set production
|
# Set production
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 rsync git-lfs && git lfs install && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy only the necessary files
|
# Copy only the necessary files
|
||||||
COPY --from=build /prod/dokploy/.next ./.next
|
COPY --from=build /prod/dokploy/.next ./.next
|
||||||
@@ -49,14 +51,14 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm
|
|||||||
# Install Nixpacks and tsx
|
# Install Nixpacks and tsx
|
||||||
# | VERBOSE=1 VERSION=1.21.0 bash
|
# | VERBOSE=1 VERSION=1.21.0 bash
|
||||||
|
|
||||||
ARG NIXPACKS_VERSION=1.29.1
|
ARG NIXPACKS_VERSION=1.39.0
|
||||||
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
||||||
&& chmod +x install.sh \
|
&& chmod +x install.sh \
|
||||||
&& ./install.sh \
|
&& ./install.sh \
|
||||||
&& pnpm install -g tsx
|
&& pnpm install -g tsx
|
||||||
|
|
||||||
# Install Railpack
|
# Install Railpack
|
||||||
ARG RAILPACK_VERSION=0.0.37
|
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,7 +1,9 @@
|
|||||||
FROM node:20.9-slim AS base
|
# syntax=docker/dockerfile:1
|
||||||
|
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,3 +1,4 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
# Build stage
|
# Build stage
|
||||||
FROM golang:1.21-alpine3.19 AS builder
|
FROM golang:1.21-alpine3.19 AS builder
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
FROM node:20.9-slim AS base
|
# syntax=docker/dockerfile:1
|
||||||
|
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,7 +1,9 @@
|
|||||||
FROM node:20.9-slim AS base
|
# syntax=docker/dockerfile:1
|
||||||
|
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.
|
||||||
@@ -19,8 +19,8 @@ See the License for the specific language governing permissions and limitations
|
|||||||
|
|
||||||
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:
|
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.
|
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Schedules, 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.
|
- **Restriction on Resale**: The multi-node support, Docker Compose file support, Schedules, 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.
|
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support, Schedules, 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.
|
For further inquiries or permissions, please contact us directly.
|
||||||
|
|||||||
118
README.md
118
README.md
@@ -1,23 +1,36 @@
|
|||||||
<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 />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div align="center" markdown="1">
|
||||||
|
<sup>Special thanks to:</sup>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<a href="https://tuple.app/dokploy">
|
||||||
|
<img src=".github/sponsors/tuple.png" alt="Tuple's sponsorship image" width="400"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### [Tuple, the premier screen sharing app for developers](https://tuple.app/dokploy)
|
||||||
|
[Available for MacOS & Windows](https://tuple.app/dokploy)<br>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
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 +60,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,57 +74,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; gap: 30px; flex-wrap: wrap;">
|
<div>
|
||||||
<a href="https://supafort.com/?ref=dokploy" target="_blank"><img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" width="190"/></a>
|
<a href="https://supafort.com/?ref=dokploy"><img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" width="300"/></a>
|
||||||
|
<a href="https://agentdock.ai/?ref=dokploy"><img src=".github/sponsors/agentdock.png" alt="agentdock.ai" width="100"/></a>
|
||||||
</div>
|
</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;">
|
<div>
|
||||||
<a href="https://lightspeed.run/?ref=dokploy"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
|
<a href="https://americancloud.com/?ref=dokploy"><img src=".github/sponsors/american-cloud.png" alt="AmericanCloud" width="300"/></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://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>
|
||||||
<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>
|
</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:
|
||||||
|
|
||||||
@@ -120,28 +123,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>
|
||||||
|
|
||||||
<!-- ## Supported OS
|
## 📺 Video Tutorial
|
||||||
|
|
||||||
- Ubuntu 24.04 LTS
|
<a href="https://youtu.be/mznYKPvhcfw">
|
||||||
- Ubuntu 23.10
|
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400"/>
|
||||||
- Ubuntu 22.04 LTS
|
</a>
|
||||||
- Ubuntu 20.04 LTS
|
|
||||||
- Ubuntu 18.04 LTS
|
|
||||||
- Debian 12
|
|
||||||
- Debian 11
|
|
||||||
- Fedora 40
|
|
||||||
- Centos 9
|
|
||||||
- Centos 8 -->
|
|
||||||
|
|
||||||
## Contributing
|
## 🤝 Contributing
|
||||||
|
|
||||||
Check out the [Contributing Guide](CONTRIBUTING.md) for more information.
|
Check out the [Contributing Guide](CONTRIBUTING.md) for more information.
|
||||||
|
|||||||
28
SECURITY.md
Normal file
28
SECURITY.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Dokploy Security Policy
|
||||||
|
|
||||||
|
At Dokploy, security is a top priority. We appreciate the help of security researchers and the community in identifying and reporting vulnerabilities.
|
||||||
|
|
||||||
|
## How to Report a Vulnerability
|
||||||
|
|
||||||
|
If you have discovered a security vulnerability in Dokploy, we ask that you report it responsibly by following these guidelines:
|
||||||
|
|
||||||
|
1. **Contact us:** Send an email to [contact@dokploy.com](mailto:contact@dokploy.com).
|
||||||
|
2. **Provide clear details:** Include as much information as possible to help us understand and reproduce the vulnerability. This should include:
|
||||||
|
* A clear description of the vulnerability.
|
||||||
|
* Steps to reproduce the vulnerability.
|
||||||
|
* Any sample code, screenshots, or videos that might be helpful.
|
||||||
|
* The potential impact of the vulnerability.
|
||||||
|
3. **Do not make the vulnerability public:** Please refrain from publicly disclosing the vulnerability until we have had the opportunity to investigate and address it. This is crucial for protecting our users.
|
||||||
|
4. **Allow us time:** We will endeavor to acknowledge receipt of your report as soon as possible and keep you informed of our progress. The time to resolve the vulnerability may vary depending on its complexity and severity.
|
||||||
|
|
||||||
|
## What We Expect From You
|
||||||
|
|
||||||
|
* Do not access user data or systems beyond what is necessary to demonstrate the vulnerability.
|
||||||
|
* Do not perform denial-of-service (DoS) attacks, spamming, or social engineering.
|
||||||
|
* Do not modify or destroy data that does not belong to you.
|
||||||
|
|
||||||
|
## Our Commitment
|
||||||
|
|
||||||
|
We are committed to working with you quickly and responsibly to address any legitimate security vulnerability.
|
||||||
|
|
||||||
|
Thank you for helping us keep Dokploy secure for everyone.
|
||||||
@@ -9,25 +9,30 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"inngest": "3.40.1",
|
||||||
|
"@dokploy/server": "workspace:*",
|
||||||
|
"@hono/node-server": "^1.14.3",
|
||||||
|
"@hono/zod-validator": "0.3.0",
|
||||||
|
"@nerimity/mimiqueue": "1.2.3",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"hono": "^4.7.10",
|
||||||
"pino": "9.4.0",
|
"pino": "9.4.0",
|
||||||
"pino-pretty": "11.2.2",
|
"pino-pretty": "11.2.2",
|
||||||
"@hono/zod-validator": "0.3.0",
|
|
||||||
"zod": "^3.23.4",
|
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"@dokploy/server": "workspace:*",
|
|
||||||
"@hono/node-server": "^1.12.1",
|
|
||||||
"hono": "^4.5.8",
|
|
||||||
"dotenv": "^16.3.1",
|
|
||||||
"redis": "4.7.0",
|
"redis": "4.7.0",
|
||||||
"@nerimity/mimiqueue": "1.2.3"
|
"zod": "^3.25.32"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.4.2",
|
"@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",
|
||||||
"@types/node": "^20.11.17",
|
"tsx": "^4.16.2",
|
||||||
"tsx": "^4.7.1"
|
"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,90 @@ 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 {
|
||||||
|
cancelDeploymentSchema,
|
||||||
|
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,
|
||||||
|
cancelOn: [
|
||||||
|
{
|
||||||
|
event: "deployment/cancelled",
|
||||||
|
if: "async.data.applicationId == event.data.applicationId || async.data.composeId == event.data.composeId",
|
||||||
|
timeout: "1h", // Allow cancellation for up to 1 hour
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ 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 +95,97 @@ 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.post(
|
||||||
|
"/cancel-deployment",
|
||||||
|
zValidator("json", cancelDeploymentSchema),
|
||||||
|
async (c) => {
|
||||||
|
const data = c.req.valid("json");
|
||||||
|
logger.info("Received cancel deployment request", data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send cancellation event to Inngest
|
||||||
|
|
||||||
|
await inngest.send({
|
||||||
|
name: "deployment/cancelled",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
const identifier =
|
||||||
|
data.applicationType === "application"
|
||||||
|
? `applicationId: ${data.applicationId}`
|
||||||
|
: `composeId: ${data.composeId}`;
|
||||||
|
|
||||||
|
logger.info("Deployment cancellation event sent", {
|
||||||
|
...data,
|
||||||
|
identifier,
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
message: "Deployment cancellation requested",
|
||||||
|
applicationType: data.applicationType,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to send deployment cancellation event", error);
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
message: "Failed to cancel 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 });
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { z } from "zod";
|
|||||||
export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||||
z.object({
|
z.object({
|
||||||
applicationId: z.string(),
|
applicationId: z.string(),
|
||||||
titleLog: z.string(),
|
titleLog: z.string().optional(),
|
||||||
descriptionLog: z.string(),
|
descriptionLog: z.string().optional(),
|
||||||
server: z.boolean().optional(),
|
server: z.boolean().optional(),
|
||||||
type: z.enum(["deploy", "redeploy"]),
|
type: z.enum(["deploy", "redeploy"]),
|
||||||
applicationType: z.literal("application"),
|
applicationType: z.literal("application"),
|
||||||
@@ -12,8 +12,8 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
|||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
composeId: z.string(),
|
composeId: z.string(),
|
||||||
titleLog: z.string(),
|
titleLog: z.string().optional(),
|
||||||
descriptionLog: z.string(),
|
descriptionLog: z.string().optional(),
|
||||||
server: z.boolean().optional(),
|
server: z.boolean().optional(),
|
||||||
type: z.enum(["deploy", "redeploy"]),
|
type: z.enum(["deploy", "redeploy"]),
|
||||||
applicationType: z.literal("compose"),
|
applicationType: z.literal("compose"),
|
||||||
@@ -22,8 +22,8 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
|||||||
z.object({
|
z.object({
|
||||||
applicationId: z.string(),
|
applicationId: z.string(),
|
||||||
previewDeploymentId: z.string(),
|
previewDeploymentId: z.string(),
|
||||||
titleLog: z.string(),
|
titleLog: z.string().optional(),
|
||||||
descriptionLog: z.string(),
|
descriptionLog: z.string().optional(),
|
||||||
server: z.boolean().optional(),
|
server: z.boolean().optional(),
|
||||||
type: z.enum(["deploy"]),
|
type: z.enum(["deploy"]),
|
||||||
applicationType: z.literal("application-preview"),
|
applicationType: z.literal("application-preview"),
|
||||||
@@ -32,3 +32,16 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
export type DeployJob = z.infer<typeof deployJobSchema>;
|
export type DeployJob = z.infer<typeof deployJobSchema>;
|
||||||
|
|
||||||
|
export const cancelDeploymentSchema = z.discriminatedUnion("applicationType", [
|
||||||
|
z.object({
|
||||||
|
applicationId: z.string(),
|
||||||
|
applicationType: z.literal("application"),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
composeId: z.string(),
|
||||||
|
applicationType: z.literal("compose"),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type CancelDeploymentJob = z.infer<typeof cancelDeploymentSchema>;
|
||||||
|
|||||||
@@ -18,14 +18,14 @@ export const deploy = async (job: DeployJob) => {
|
|||||||
if (job.type === "redeploy") {
|
if (job.type === "redeploy") {
|
||||||
await rebuildRemoteApplication({
|
await rebuildRemoteApplication({
|
||||||
applicationId: job.applicationId,
|
applicationId: job.applicationId,
|
||||||
titleLog: job.titleLog,
|
titleLog: job.titleLog || "Rebuild deployment",
|
||||||
descriptionLog: job.descriptionLog,
|
descriptionLog: job.descriptionLog || "",
|
||||||
});
|
});
|
||||||
} else if (job.type === "deploy") {
|
} else if (job.type === "deploy") {
|
||||||
await deployRemoteApplication({
|
await deployRemoteApplication({
|
||||||
applicationId: job.applicationId,
|
applicationId: job.applicationId,
|
||||||
titleLog: job.titleLog,
|
titleLog: job.titleLog || "Manual deployment",
|
||||||
descriptionLog: job.descriptionLog,
|
descriptionLog: job.descriptionLog || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,14 +38,14 @@ export const deploy = async (job: DeployJob) => {
|
|||||||
if (job.type === "redeploy") {
|
if (job.type === "redeploy") {
|
||||||
await rebuildRemoteCompose({
|
await rebuildRemoteCompose({
|
||||||
composeId: job.composeId,
|
composeId: job.composeId,
|
||||||
titleLog: job.titleLog,
|
titleLog: job.titleLog || "Rebuild deployment",
|
||||||
descriptionLog: job.descriptionLog,
|
descriptionLog: job.descriptionLog || "",
|
||||||
});
|
});
|
||||||
} else if (job.type === "deploy") {
|
} else if (job.type === "deploy") {
|
||||||
await deployRemoteCompose({
|
await deployRemoteCompose({
|
||||||
composeId: job.composeId,
|
composeId: job.composeId,
|
||||||
titleLog: job.titleLog,
|
titleLog: job.titleLog || "Manual deployment",
|
||||||
descriptionLog: job.descriptionLog,
|
descriptionLog: job.descriptionLog || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,14 +57,14 @@ export const deploy = async (job: DeployJob) => {
|
|||||||
if (job.type === "deploy") {
|
if (job.type === "deploy") {
|
||||||
await deployRemotePreviewApplication({
|
await deployRemotePreviewApplication({
|
||||||
applicationId: job.applicationId,
|
applicationId: job.applicationId,
|
||||||
titleLog: job.titleLog,
|
titleLog: job.titleLog || "Preview Deployment",
|
||||||
descriptionLog: job.descriptionLog,
|
descriptionLog: job.descriptionLog || "",
|
||||||
previewDeploymentId: job.previewDeploymentId,
|
previewDeploymentId: job.previewDeploymentId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} 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 +1 @@
|
|||||||
20.9.0
|
20.16.0
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
FROM node:18-slim AS base
|
|
||||||
ENV PNPM_HOME="/pnpm"
|
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
|
||||||
RUN corepack enable
|
|
||||||
|
|
||||||
FROM base AS build
|
|
||||||
COPY . /usr/src/app
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
# Build only the dokploy app
|
|
||||||
RUN pnpm run dokploy:build
|
|
||||||
|
|
||||||
# Deploy only the dokploy app
|
|
||||||
RUN pnpm deploy --filter=dokploy --prod /prod/dokploy
|
|
||||||
|
|
||||||
FROM base AS dokploy
|
|
||||||
COPY --from=build /prod/dokploy /prod/dokploy
|
|
||||||
WORKDIR /prod/dokploy
|
|
||||||
EXPOSE 3000
|
|
||||||
CMD [ "pnpm", "start" ]
|
|
||||||
@@ -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,7 +1,7 @@
|
|||||||
import { addSuffixToAllProperties } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { addSuffixToAllProperties } from "@dokploy/server";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFile1 = `
|
const composeFile1 = `
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
@@ -61,7 +61,7 @@ secrets:
|
|||||||
file: ./db_password.txt
|
file: ./db_password.txt
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFile1 = load(`
|
const expectedComposeFile1 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -120,7 +120,7 @@ secrets:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to all properties in compose file 1", () => {
|
test("Add suffix to all properties in compose file 1", () => {
|
||||||
const composeData = load(composeFile1) as ComposeSpecification;
|
const composeData = parse(composeFile1) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
||||||
@@ -185,7 +185,7 @@ secrets:
|
|||||||
file: ./db_password.txt
|
file: ./db_password.txt
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFile2 = load(`
|
const expectedComposeFile2 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -243,7 +243,7 @@ secrets:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to all properties in compose file 2", () => {
|
test("Add suffix to all properties in compose file 2", () => {
|
||||||
const composeData = load(composeFile2) as ComposeSpecification;
|
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
||||||
@@ -308,7 +308,7 @@ secrets:
|
|||||||
file: ./service_secret.txt
|
file: ./service_secret.txt
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFile3 = load(`
|
const expectedComposeFile3 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -366,7 +366,7 @@ secrets:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to all properties in compose file 3", () => {
|
test("Add suffix to all properties in compose file 3", () => {
|
||||||
const composeData = load(composeFile3) as ComposeSpecification;
|
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
||||||
@@ -420,7 +420,7 @@ volumes:
|
|||||||
driver: local
|
driver: local
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFile = load(`
|
const expectedComposeFile = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -467,7 +467,7 @@ volumes:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to all properties in Plausible compose file", () => {
|
test("Add suffix to all properties in Plausible compose file", () => {
|
||||||
const composeData = load(composeFile) as ComposeSpecification;
|
const composeData = parse(composeFile) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToConfigsRoot } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { addSuffixToConfigsRoot, generateRandomHash } from "@dokploy/server";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
test("Generate random hash with 8 characters", () => {
|
test("Generate random hash with 8 characters", () => {
|
||||||
const hash = generateRandomHash();
|
const hash = generateRandomHash();
|
||||||
@@ -24,7 +23,7 @@ configs:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to configs in root property", () => {
|
test("Add suffix to configs in root property", () => {
|
||||||
const composeData = load(composeFile) as ComposeSpecification;
|
const composeData = parse(composeFile) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -60,7 +59,7 @@ configs:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to multiple configs in root property", () => {
|
test("Add suffix to multiple configs in root property", () => {
|
||||||
const composeData = load(composeFileMultipleConfigs) as ComposeSpecification;
|
const composeData = parse(composeFileMultipleConfigs) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -93,7 +92,7 @@ configs:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to configs with different properties in root property", () => {
|
test("Add suffix to configs with different properties in root property", () => {
|
||||||
const composeData = load(
|
const composeData = parse(
|
||||||
composeFileDifferentProperties,
|
composeFileDifferentProperties,
|
||||||
) as ComposeSpecification;
|
) as ComposeSpecification;
|
||||||
|
|
||||||
@@ -138,7 +137,7 @@ configs:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// Expected compose file con el prefijo `testhash`
|
// Expected compose file con el prefijo `testhash`
|
||||||
const expectedComposeFileConfigRoot = load(`
|
const expectedComposeFileConfigRoot = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -163,7 +162,7 @@ configs:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to configs in root property", () => {
|
test("Add suffix to configs in root property", () => {
|
||||||
const composeData = load(composeFileConfigRoot) as ComposeSpecification;
|
const composeData = parse(composeFileConfigRoot) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToConfigsInServices } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import {
|
||||||
|
addSuffixToConfigsInServices,
|
||||||
|
generateRandomHash,
|
||||||
|
} from "@dokploy/server";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFile = `
|
const composeFile = `
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
@@ -20,7 +22,7 @@ configs:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to configs in services", () => {
|
test("Add suffix to configs in services", () => {
|
||||||
const composeData = load(composeFile) as ComposeSpecification;
|
const composeData = parse(composeFile) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -52,7 +54,7 @@ configs:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to configs in services with single config", () => {
|
test("Add suffix to configs in services with single config", () => {
|
||||||
const composeData = load(
|
const composeData = parse(
|
||||||
composeFileSingleServiceConfig,
|
composeFileSingleServiceConfig,
|
||||||
) as ComposeSpecification;
|
) as ComposeSpecification;
|
||||||
|
|
||||||
@@ -106,7 +108,7 @@ configs:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to configs in services with multiple configs", () => {
|
test("Add suffix to configs in services with multiple configs", () => {
|
||||||
const composeData = load(
|
const composeData = parse(
|
||||||
composeFileMultipleServicesConfigs,
|
composeFileMultipleServicesConfigs,
|
||||||
) as ComposeSpecification;
|
) as ComposeSpecification;
|
||||||
|
|
||||||
@@ -155,7 +157,7 @@ services:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// Expected compose file con el prefijo `testhash`
|
// Expected compose file con el prefijo `testhash`
|
||||||
const expectedComposeFileConfigServices = load(`
|
const expectedComposeFileConfigServices = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -180,7 +182,7 @@ services:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to configs in services", () => {
|
test("Add suffix to configs in services", () => {
|
||||||
const composeData = load(composeFileConfigServices) as ComposeSpecification;
|
const composeData = parse(composeFileConfigServices) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToAllConfigs } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { addSuffixToAllConfigs, generateRandomHash } from "@dokploy/server";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
test("Generate random hash with 8 characters", () => {
|
test("Generate random hash with 8 characters", () => {
|
||||||
const hash = generateRandomHash();
|
const hash = generateRandomHash();
|
||||||
@@ -44,7 +43,7 @@ configs:
|
|||||||
file: ./db-config.yml
|
file: ./db-config.yml
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFileCombinedConfigs = load(`
|
const expectedComposeFileCombinedConfigs = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -78,7 +77,7 @@ configs:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to all configs in root and services", () => {
|
test("Add suffix to all configs in root and services", () => {
|
||||||
const composeData = load(composeFileCombinedConfigs) as ComposeSpecification;
|
const composeData = parse(composeFileCombinedConfigs) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
@@ -123,7 +122,7 @@ configs:
|
|||||||
file: ./db-config.yml
|
file: ./db-config.yml
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFileWithEnvAndExternal = load(`
|
const expectedComposeFileWithEnvAndExternal = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -160,7 +159,7 @@ configs:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to configs with environment and external", () => {
|
test("Add suffix to configs with environment and external", () => {
|
||||||
const composeData = load(
|
const composeData = parse(
|
||||||
composeFileWithEnvAndExternal,
|
composeFileWithEnvAndExternal,
|
||||||
) as ComposeSpecification;
|
) as ComposeSpecification;
|
||||||
|
|
||||||
@@ -201,7 +200,7 @@ configs:
|
|||||||
file: ./db-config.yml
|
file: ./db-config.yml
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFileWithTemplateDriverAndLabels = load(`
|
const expectedComposeFileWithTemplateDriverAndLabels = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -232,7 +231,7 @@ configs:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to configs with template driver and labels", () => {
|
test("Add suffix to configs with template driver and labels", () => {
|
||||||
const composeData = load(
|
const composeData = parse(
|
||||||
composeFileWithTemplateDriverAndLabels,
|
composeFileWithTemplateDriverAndLabels,
|
||||||
) as ComposeSpecification;
|
) as ComposeSpecification;
|
||||||
|
|
||||||
|
|||||||
@@ -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,8 +1,7 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToNetworksRoot } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { addSuffixToNetworksRoot, generateRandomHash } from "@dokploy/server";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFile = `
|
const composeFile = `
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
@@ -36,7 +35,7 @@ test("Generate random hash with 8 characters", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Add suffix to networks root property", () => {
|
test("Add suffix to networks root property", () => {
|
||||||
const composeData = load(composeFile) as ComposeSpecification;
|
const composeData = parse(composeFile) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -80,7 +79,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to advanced networks root property (2 TRY)", () => {
|
test("Add suffix to advanced networks root property (2 TRY)", () => {
|
||||||
const composeData = load(composeFile2) as ComposeSpecification;
|
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -121,7 +120,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to networks with external properties", () => {
|
test("Add suffix to networks with external properties", () => {
|
||||||
const composeData = load(composeFile3) as ComposeSpecification;
|
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -161,7 +160,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to networks with IPAM configurations", () => {
|
test("Add suffix to networks with IPAM configurations", () => {
|
||||||
const composeData = load(composeFile4) as ComposeSpecification;
|
const composeData = parse(composeFile4) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -202,7 +201,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to networks with custom options", () => {
|
test("Add suffix to networks with custom options", () => {
|
||||||
const composeData = load(composeFile5) as ComposeSpecification;
|
const composeData = parse(composeFile5) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -265,7 +264,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to networks with static suffix", () => {
|
test("Add suffix to networks with static suffix", () => {
|
||||||
const composeData = load(composeFile6) as ComposeSpecification;
|
const composeData = parse(composeFile6) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
@@ -274,7 +273,7 @@ test("Add suffix to networks with static suffix", () => {
|
|||||||
}
|
}
|
||||||
const networks = addSuffixToNetworksRoot(composeData.networks, suffix);
|
const networks = addSuffixToNetworksRoot(composeData.networks, suffix);
|
||||||
|
|
||||||
const expectedComposeData = load(
|
const expectedComposeData = parse(
|
||||||
expectedComposeFile6,
|
expectedComposeFile6,
|
||||||
) as ComposeSpecification;
|
) as ComposeSpecification;
|
||||||
expect(networks).toStrictEqual(expectedComposeData.networks);
|
expect(networks).toStrictEqual(expectedComposeData.networks);
|
||||||
@@ -294,7 +293,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("It shoudn't add suffix to dokploy-network", () => {
|
test("It shoudn't add suffix to dokploy-network", () => {
|
||||||
const composeData = load(composeFile7) as ComposeSpecification;
|
const composeData = parse(composeFile7) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToServiceNetworks } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import {
|
||||||
|
addSuffixToServiceNetworks,
|
||||||
|
generateRandomHash,
|
||||||
|
} from "@dokploy/server";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFile = `
|
const composeFile = `
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
@@ -21,7 +23,7 @@ services:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to networks in services", () => {
|
test("Add suffix to networks in services", () => {
|
||||||
const composeData = load(composeFile) as ComposeSpecification;
|
const composeData = parse(composeFile) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -65,7 +67,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to networks in services with aliases", () => {
|
test("Add suffix to networks in services with aliases", () => {
|
||||||
const composeData = load(composeFile2) as ComposeSpecification;
|
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -105,7 +107,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to networks in services (Object with simple networks)", () => {
|
test("Add suffix to networks in services (Object with simple networks)", () => {
|
||||||
const composeData = load(composeFile3) as ComposeSpecification;
|
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -151,7 +153,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to networks in services (combined case)", () => {
|
test("Add suffix to networks in services (combined case)", () => {
|
||||||
const composeData = load(composeFileCombined) as ComposeSpecification;
|
const composeData = parse(composeFileCombined) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -194,7 +196,7 @@ services:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("It shoudn't add suffix to dokploy-network in services", () => {
|
test("It shoudn't add suffix to dokploy-network in services", () => {
|
||||||
const composeData = load(composeFile7) as ComposeSpecification;
|
const composeData = parse(composeFile7) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -243,7 +245,7 @@ services:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("It shoudn't add suffix to dokploy-network in services multiples cases", () => {
|
test("It shoudn't add suffix to dokploy-network in services multiples cases", () => {
|
||||||
const composeData = load(composeFile8) as ComposeSpecification;
|
const composeData = parse(composeFile8) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
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 { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFileCombined = `
|
const composeFileCombined = `
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
@@ -39,7 +39,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to networks in services and root (combined case)", () => {
|
test("Add suffix to networks in services and root (combined case)", () => {
|
||||||
const composeData = load(composeFileCombined) as ComposeSpecification;
|
const composeData = parse(composeFileCombined) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ test("Add suffix to networks in services and root (combined case)", () => {
|
|||||||
expect(redisNetworks).not.toHaveProperty("backend");
|
expect(redisNetworks).not.toHaveProperty("backend");
|
||||||
});
|
});
|
||||||
|
|
||||||
const expectedComposeFile = load(`
|
const expectedComposeFile = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -120,7 +120,7 @@ networks:
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
test("Add suffix to networks in compose file", () => {
|
test("Add suffix to networks in compose file", () => {
|
||||||
const composeData = load(composeFileCombined) as ComposeSpecification;
|
const composeData = parse(composeFileCombined) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
if (!composeData?.networks) {
|
if (!composeData?.networks) {
|
||||||
@@ -156,7 +156,7 @@ networks:
|
|||||||
driver: bridge
|
driver: bridge
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFile2 = load(`
|
const expectedComposeFile2 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -182,7 +182,7 @@ networks:
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
test("Add suffix to networks in compose file with external and internal networks", () => {
|
test("Add suffix to networks in compose file with external and internal networks", () => {
|
||||||
const composeData = load(composeFile2) as ComposeSpecification;
|
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
|
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
|
||||||
@@ -218,7 +218,7 @@ networks:
|
|||||||
com.docker.network.bridge.enable_icc: "true"
|
com.docker.network.bridge.enable_icc: "true"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFile3 = load(`
|
const expectedComposeFile3 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -247,7 +247,7 @@ networks:
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
test("Add suffix to networks in compose file with multiple services and complex network configurations", () => {
|
test("Add suffix to networks in compose file with multiple services and complex network configurations", () => {
|
||||||
const composeData = load(composeFile3) as ComposeSpecification;
|
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
|
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
|
||||||
@@ -289,7 +289,7 @@ networks:
|
|||||||
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFile4 = load(`
|
const expectedComposeFile4 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -326,7 +326,7 @@ networks:
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
test("Expect don't add suffix to dokploy-network in compose file with multiple services and complex network configurations", () => {
|
test("Expect don't add suffix to dokploy-network in compose file with multiple services and complex network configurations", () => {
|
||||||
const composeData = load(composeFile4) as ComposeSpecification;
|
const composeData = parse(composeFile4) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
|
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToSecretsRoot } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { addSuffixToSecretsRoot, generateRandomHash } from "@dokploy/server";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
test("Generate random hash with 8 characters", () => {
|
test("Generate random hash with 8 characters", () => {
|
||||||
const hash = generateRandomHash();
|
const hash = generateRandomHash();
|
||||||
@@ -24,7 +23,7 @@ secrets:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to secrets in root property", () => {
|
test("Add suffix to secrets in root property", () => {
|
||||||
const composeData = load(composeFileSecretsRoot) as ComposeSpecification;
|
const composeData = parse(composeFileSecretsRoot) as ComposeSpecification;
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
if (!composeData?.secrets) {
|
if (!composeData?.secrets) {
|
||||||
@@ -53,7 +52,7 @@ secrets:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to secrets in root property (Test 1)", () => {
|
test("Add suffix to secrets in root property (Test 1)", () => {
|
||||||
const composeData = load(composeFileSecretsRoot1) as ComposeSpecification;
|
const composeData = parse(composeFileSecretsRoot1) as ComposeSpecification;
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
if (!composeData?.secrets) {
|
if (!composeData?.secrets) {
|
||||||
@@ -85,7 +84,7 @@ secrets:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to secrets in root property (Test 2)", () => {
|
test("Add suffix to secrets in root property (Test 2)", () => {
|
||||||
const composeData = load(composeFileSecretsRoot2) as ComposeSpecification;
|
const composeData = parse(composeFileSecretsRoot2) as ComposeSpecification;
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
if (!composeData?.secrets) {
|
if (!composeData?.secrets) {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToSecretsInServices } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import {
|
||||||
|
addSuffixToSecretsInServices,
|
||||||
|
generateRandomHash,
|
||||||
|
} from "@dokploy/server";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFileSecretsServices = `
|
const composeFileSecretsServices = `
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
@@ -19,7 +21,7 @@ secrets:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to secrets in services", () => {
|
test("Add suffix to secrets in services", () => {
|
||||||
const composeData = load(composeFileSecretsServices) as ComposeSpecification;
|
const composeData = parse(composeFileSecretsServices) as ComposeSpecification;
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
if (!composeData.services) {
|
if (!composeData.services) {
|
||||||
@@ -52,7 +54,9 @@ secrets:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to secrets in services (Test 1)", () => {
|
test("Add suffix to secrets in services (Test 1)", () => {
|
||||||
const composeData = load(composeFileSecretsServices1) as ComposeSpecification;
|
const composeData = parse(
|
||||||
|
composeFileSecretsServices1,
|
||||||
|
) as ComposeSpecification;
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
if (!composeData.services) {
|
if (!composeData.services) {
|
||||||
@@ -91,7 +95,9 @@ secrets:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to secrets in services (Test 2)", () => {
|
test("Add suffix to secrets in services (Test 2)", () => {
|
||||||
const composeData = load(composeFileSecretsServices2) as ComposeSpecification;
|
const composeData = parse(
|
||||||
|
composeFileSecretsServices2,
|
||||||
|
) as ComposeSpecification;
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
if (!composeData.services) {
|
if (!composeData.services) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { addSuffixToAllSecrets } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { addSuffixToAllSecrets } from "@dokploy/server";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFileCombinedSecrets = `
|
const composeFileCombinedSecrets = `
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
@@ -25,7 +25,7 @@ secrets:
|
|||||||
file: ./app_secret.txt
|
file: ./app_secret.txt
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFileCombinedSecrets = load(`
|
const expectedComposeFileCombinedSecrets = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -48,7 +48,7 @@ secrets:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to all secrets", () => {
|
test("Add suffix to all secrets", () => {
|
||||||
const composeData = load(composeFileCombinedSecrets) as ComposeSpecification;
|
const composeData = parse(composeFileCombinedSecrets) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
|
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
|
||||||
@@ -77,7 +77,7 @@ secrets:
|
|||||||
file: ./cache_secret.txt
|
file: ./cache_secret.txt
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFileCombinedSecrets3 = load(`
|
const expectedComposeFileCombinedSecrets3 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -99,7 +99,9 @@ secrets:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to all secrets (3rd Case)", () => {
|
test("Add suffix to all secrets (3rd Case)", () => {
|
||||||
const composeData = load(composeFileCombinedSecrets3) as ComposeSpecification;
|
const composeData = parse(
|
||||||
|
composeFileCombinedSecrets3,
|
||||||
|
) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
|
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
|
||||||
@@ -128,7 +130,7 @@ secrets:
|
|||||||
file: ./db_password.txt
|
file: ./db_password.txt
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFileCombinedSecrets4 = load(`
|
const expectedComposeFileCombinedSecrets4 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -150,7 +152,9 @@ secrets:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to all secrets (4th Case)", () => {
|
test("Add suffix to all secrets (4th Case)", () => {
|
||||||
const composeData = load(composeFileCombinedSecrets4) as ComposeSpecification;
|
const composeData = parse(
|
||||||
|
composeFileCombinedSecrets4,
|
||||||
|
) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
|
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFile = `
|
const composeFile = `
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
@@ -28,7 +27,7 @@ test("Generate random hash with 8 characters", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Add suffix to service names with container_name in compose file", () => {
|
test("Add suffix to service names with container_name in compose file", () => {
|
||||||
const composeData = load(composeFile) as ComposeSpecification;
|
const composeData = parse(composeFile) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
test("Generate random hash with 8 characters", () => {
|
test("Generate random hash with 8 characters", () => {
|
||||||
const hash = generateRandomHash();
|
const hash = generateRandomHash();
|
||||||
@@ -33,7 +32,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to service names with depends_on (array) in compose file", () => {
|
test("Add suffix to service names with depends_on (array) in compose file", () => {
|
||||||
const composeData = load(composeFile4) as ComposeSpecification;
|
const composeData = parse(composeFile4) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -103,7 +102,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to service names with depends_on (object) in compose file", () => {
|
test("Add suffix to service names with depends_on (object) in compose file", () => {
|
||||||
const composeData = load(composeFile5) as ComposeSpecification;
|
const composeData = parse(composeFile5) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
test("Generate random hash with 8 characters", () => {
|
test("Generate random hash with 8 characters", () => {
|
||||||
const hash = generateRandomHash();
|
const hash = generateRandomHash();
|
||||||
@@ -31,7 +30,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to service names with extends (string) in compose file", () => {
|
test("Add suffix to service names with extends (string) in compose file", () => {
|
||||||
const composeData = load(composeFile6) as ComposeSpecification;
|
const composeData = parse(composeFile6) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -91,7 +90,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to service names with extends (object) in compose file", () => {
|
test("Add suffix to service names with extends (object) in compose file", () => {
|
||||||
const composeData = load(composeFile7) as ComposeSpecification;
|
const composeData = parse(composeFile7) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
test("Generate random hash with 8 characters", () => {
|
test("Generate random hash with 8 characters", () => {
|
||||||
const hash = generateRandomHash();
|
const hash = generateRandomHash();
|
||||||
@@ -32,7 +31,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to service names with links in compose file", () => {
|
test("Add suffix to service names with links in compose file", () => {
|
||||||
const composeData = load(composeFile2) as ComposeSpecification;
|
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
test("Generate random hash with 8 characters", () => {
|
test("Generate random hash with 8 characters", () => {
|
||||||
const hash = generateRandomHash();
|
const hash = generateRandomHash();
|
||||||
@@ -27,7 +26,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to service names in compose file", () => {
|
test("Add suffix to service names in compose file", () => {
|
||||||
const composeData = load(composeFile) as ComposeSpecification;
|
const composeData = parse(composeFile) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
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 { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFileCombinedAllCases = `
|
const composeFileCombinedAllCases = `
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
@@ -38,7 +38,7 @@ networks:
|
|||||||
driver: bridge
|
driver: bridge
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFile = load(`
|
const expectedComposeFile = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -71,7 +71,9 @@ networks:
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
test("Add suffix to all service names in compose file", () => {
|
test("Add suffix to all service names in compose file", () => {
|
||||||
const composeData = load(composeFileCombinedAllCases) as ComposeSpecification;
|
const composeData = parse(
|
||||||
|
composeFileCombinedAllCases,
|
||||||
|
) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
@@ -131,7 +133,7 @@ networks:
|
|||||||
driver: bridge
|
driver: bridge
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFile1 = load(`
|
const expectedComposeFile1 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -176,7 +178,7 @@ networks:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to all service names in compose file 1", () => {
|
test("Add suffix to all service names in compose file 1", () => {
|
||||||
const composeData = load(composeFile1) as ComposeSpecification;
|
const composeData = parse(composeFile1) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
|
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
|
||||||
@@ -227,7 +229,7 @@ networks:
|
|||||||
driver: bridge
|
driver: bridge
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFile2 = load(`
|
const expectedComposeFile2 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -271,7 +273,7 @@ networks:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to all service names in compose file 2", () => {
|
test("Add suffix to all service names in compose file 2", () => {
|
||||||
const composeData = load(composeFile2) as ComposeSpecification;
|
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
|
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
|
||||||
@@ -322,7 +324,7 @@ networks:
|
|||||||
driver: bridge
|
driver: bridge
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFile3 = load(`
|
const expectedComposeFile3 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -366,7 +368,7 @@ networks:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to all service names in compose file 3", () => {
|
test("Add suffix to all service names in compose file 3", () => {
|
||||||
const composeData = load(composeFile3) as ComposeSpecification;
|
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
|
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
test("Generate random hash with 8 characters", () => {
|
test("Generate random hash with 8 characters", () => {
|
||||||
const hash = generateRandomHash();
|
const hash = generateRandomHash();
|
||||||
@@ -36,7 +35,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to service names with volumes_from in compose file", () => {
|
test("Add suffix to service names with volumes_from in compose file", () => {
|
||||||
const composeData = load(composeFile3) as ComposeSpecification;
|
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToAllVolumes, addSuffixToVolumesRoot } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import {
|
||||||
|
addSuffixToAllVolumes,
|
||||||
|
addSuffixToVolumesRoot,
|
||||||
|
generateRandomHash,
|
||||||
|
} from "@dokploy/server";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFile = `
|
const composeFile = `
|
||||||
services:
|
services:
|
||||||
@@ -67,7 +70,7 @@ volumes:
|
|||||||
driver: local
|
driver: local
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedDockerCompose = load(`
|
const expectedDockerCompose = parse(`
|
||||||
services:
|
services:
|
||||||
mail:
|
mail:
|
||||||
image: bytemark/smtp
|
image: bytemark/smtp
|
||||||
@@ -140,7 +143,7 @@ test("Generate random hash with 8 characters", () => {
|
|||||||
// Docker compose needs unique names for services, volumes, networks and containers
|
// Docker compose needs unique names for services, volumes, networks and containers
|
||||||
// So base on a input which is a dockercompose file, it should replace the name with a hash and return a new dockercompose file
|
// So base on a input which is a dockercompose file, it should replace the name with a hash and return a new dockercompose file
|
||||||
test("Add suffix to volumes root property", () => {
|
test("Add suffix to volumes root property", () => {
|
||||||
const composeData = load(composeFile) as ComposeSpecification;
|
const composeData = parse(composeFile) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -162,7 +165,7 @@ test("Add suffix to volumes root property", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Expect to change the suffix in all the possible places", () => {
|
test("Expect to change the suffix in all the possible places", () => {
|
||||||
const composeData = load(composeFile) as ComposeSpecification;
|
const composeData = parse(composeFile) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||||
@@ -192,7 +195,7 @@ volumes:
|
|||||||
mongo-data:
|
mongo-data:
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedDockerCompose2 = load(`
|
const expectedDockerCompose2 = parse(`
|
||||||
version: '3.8'
|
version: '3.8'
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
@@ -215,7 +218,7 @@ volumes:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Expect to change the suffix in all the possible places (2 Try)", () => {
|
test("Expect to change the suffix in all the possible places (2 Try)", () => {
|
||||||
const composeData = load(composeFile2) as ComposeSpecification;
|
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||||
@@ -245,7 +248,7 @@ volumes:
|
|||||||
mongo-data:
|
mongo-data:
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedDockerCompose3 = load(`
|
const expectedDockerCompose3 = parse(`
|
||||||
version: '3.8'
|
version: '3.8'
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
@@ -268,7 +271,7 @@ volumes:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Expect to change the suffix in all the possible places (3 Try)", () => {
|
test("Expect to change the suffix in all the possible places (3 Try)", () => {
|
||||||
const composeData = load(composeFile3) as ComposeSpecification;
|
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||||
@@ -642,7 +645,7 @@ volumes:
|
|||||||
db-config:
|
db-config:
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedDockerComposeComplex = load(`
|
const expectedDockerComposeComplex = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
services:
|
services:
|
||||||
studio:
|
studio:
|
||||||
@@ -1009,7 +1012,7 @@ volumes:
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
test("Expect to change the suffix in all the possible places (4 Try)", () => {
|
test("Expect to change the suffix in all the possible places (4 Try)", () => {
|
||||||
const composeData = load(composeFileComplex) as ComposeSpecification;
|
const composeData = parse(composeFileComplex) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||||
@@ -1062,7 +1065,7 @@ volumes:
|
|||||||
db-data:
|
db-data:
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedDockerComposeExample1 = load(`
|
const expectedDockerComposeExample1 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
@@ -1108,7 +1111,7 @@ volumes:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Expect to change the suffix in all the possible places (5 Try)", () => {
|
test("Expect to change the suffix in all the possible places (5 Try)", () => {
|
||||||
const composeData = load(composeFileExample1) as ComposeSpecification;
|
const composeData = parse(composeFileExample1) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||||
@@ -1140,7 +1143,7 @@ volumes:
|
|||||||
backrest-cache:
|
backrest-cache:
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedDockerComposeBackrest = load(`
|
const expectedDockerComposeBackrest = parse(`
|
||||||
services:
|
services:
|
||||||
backrest:
|
backrest:
|
||||||
image: garethgeorge/backrest:v1.7.3
|
image: garethgeorge/backrest:v1.7.3
|
||||||
@@ -1165,7 +1168,7 @@ volumes:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Should handle volume paths with subdirectories correctly", () => {
|
test("Should handle volume paths with subdirectories correctly", () => {
|
||||||
const composeData = load(composeFileBackrest) as ComposeSpecification;
|
const composeData = parse(composeFileBackrest) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToVolumesRoot } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { addSuffixToVolumesRoot, generateRandomHash } from "@dokploy/server";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFile = `
|
const composeFile = `
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
@@ -30,7 +29,7 @@ test("Generate random hash with 8 characters", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Add suffix to volumes in root property", () => {
|
test("Add suffix to volumes in root property", () => {
|
||||||
const composeData = load(composeFile) as ComposeSpecification;
|
const composeData = parse(composeFile) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -68,7 +67,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to volumes in root property (Case 2)", () => {
|
test("Add suffix to volumes in root property (Case 2)", () => {
|
||||||
const composeData = load(composeFile2) as ComposeSpecification;
|
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -102,7 +101,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to volumes in root property (Case 3)", () => {
|
test("Add suffix to volumes in root property (Case 3)", () => {
|
||||||
const composeData = load(composeFile3) as ComposeSpecification;
|
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -149,7 +148,7 @@ volumes:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// Expected compose file con el prefijo `testhash`
|
// Expected compose file con el prefijo `testhash`
|
||||||
const expectedComposeFile4 = load(`
|
const expectedComposeFile4 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -180,7 +179,7 @@ volumes:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to volumes in root property", () => {
|
test("Add suffix to volumes in root property", () => {
|
||||||
const composeData = load(composeFile4) as ComposeSpecification;
|
const composeData = parse(composeFile4) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
|
||||||
import { addSuffixToVolumesInServices } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import {
|
||||||
|
addSuffixToVolumesInServices,
|
||||||
|
generateRandomHash,
|
||||||
|
} from "@dokploy/server";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
test("Generate random hash with 8 characters", () => {
|
test("Generate random hash with 8 characters", () => {
|
||||||
const hash = generateRandomHash();
|
const hash = generateRandomHash();
|
||||||
@@ -22,7 +24,7 @@ services:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to volumes declared directly in services", () => {
|
test("Add suffix to volumes declared directly in services", () => {
|
||||||
const composeData = load(composeFile1) as ComposeSpecification;
|
const composeData = parse(composeFile1) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -57,7 +59,7 @@ volumes:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to volumes declared directly in services (Case 2)", () => {
|
test("Add suffix to volumes declared directly in services (Case 2)", () => {
|
||||||
const composeData = load(composeFileTypeVolume) as ComposeSpecification;
|
const composeData = parse(composeFileTypeVolume) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { addSuffixToAllVolumes } from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { addSuffixToAllVolumes } from "@dokploy/server";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFileTypeVolume = `
|
const composeFileTypeVolume = `
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
@@ -23,7 +23,7 @@ volumes:
|
|||||||
driver: local
|
driver: local
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFileTypeVolume = load(`
|
const expectedComposeFileTypeVolume = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -44,7 +44,7 @@ volumes:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to volumes with type: volume in services", () => {
|
test("Add suffix to volumes with type: volume in services", () => {
|
||||||
const composeData = load(composeFileTypeVolume) as ComposeSpecification;
|
const composeData = parse(composeFileTypeVolume) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ volumes:
|
|||||||
driver: local
|
driver: local
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFileTypeVolume1 = load(`
|
const expectedComposeFileTypeVolume1 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -93,7 +93,7 @@ volumes:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to mixed volumes in services", () => {
|
test("Add suffix to mixed volumes in services", () => {
|
||||||
const composeData = load(composeFileTypeVolume1) as ComposeSpecification;
|
const composeData = parse(composeFileTypeVolume1) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ volumes:
|
|||||||
device: /path/to/app/logs
|
device: /path/to/app/logs
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFileTypeVolume2 = load(`
|
const expectedComposeFileTypeVolume2 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -154,7 +154,7 @@ volumes:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to complex volume configurations in services", () => {
|
test("Add suffix to complex volume configurations in services", () => {
|
||||||
const composeData = load(composeFileTypeVolume2) as ComposeSpecification;
|
const composeData = parse(composeFileTypeVolume2) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ volumes:
|
|||||||
device: /path/to/shared/logs
|
device: /path/to/shared/logs
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFileTypeVolume3 = load(`
|
const expectedComposeFileTypeVolume3 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -273,7 +273,7 @@ volumes:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to complex nested volumes configuration in services", () => {
|
test("Add suffix to complex nested volumes configuration in services", () => {
|
||||||
const composeData = load(composeFileTypeVolume3) as ComposeSpecification;
|
const composeData = parse(composeFileTypeVolume3) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
|
|||||||
@@ -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,16 +25,21 @@ 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: "",
|
||||||
cleanCache: false,
|
cleanCache: false,
|
||||||
watchPaths: [],
|
watchPaths: [],
|
||||||
|
enableSubmodules: false,
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
|
triggerType: "push",
|
||||||
appName: "",
|
appName: "",
|
||||||
autoDeploy: true,
|
autoDeploy: true,
|
||||||
serverId: "",
|
serverId: "",
|
||||||
@@ -51,13 +56,21 @@ const baseApp: ApplicationNested = {
|
|||||||
previewPort: 3000,
|
previewPort: 3000,
|
||||||
previewLimit: 0,
|
previewLimit: 0,
|
||||||
previewWildcard: "",
|
previewWildcard: "",
|
||||||
project: {
|
environment: {
|
||||||
env: "",
|
env: "",
|
||||||
organizationId: "",
|
environmentId: "",
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
|
||||||
createdAt: "",
|
createdAt: "",
|
||||||
|
description: "",
|
||||||
projectId: "",
|
projectId: "",
|
||||||
|
project: {
|
||||||
|
env: "",
|
||||||
|
organizationId: "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
createdAt: "",
|
||||||
|
projectId: "",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
buildArgs: null,
|
buildArgs: null,
|
||||||
buildPath: "/",
|
buildPath: "/",
|
||||||
@@ -87,6 +100,7 @@ const baseApp: ApplicationNested = {
|
|||||||
dockerfile: null,
|
dockerfile: null,
|
||||||
dockerImage: null,
|
dockerImage: null,
|
||||||
dropBuildPath: null,
|
dropBuildPath: null,
|
||||||
|
environmentId: "",
|
||||||
enabled: null,
|
enabled: null,
|
||||||
env: null,
|
env: null,
|
||||||
healthCheckSwarm: null,
|
healthCheckSwarm: null,
|
||||||
@@ -101,8 +115,8 @@ const baseApp: ApplicationNested = {
|
|||||||
password: null,
|
password: null,
|
||||||
placementSwarm: null,
|
placementSwarm: null,
|
||||||
ports: [],
|
ports: [],
|
||||||
projectId: "",
|
|
||||||
publishDirectory: null,
|
publishDirectory: null,
|
||||||
|
isStaticSpa: null,
|
||||||
redirects: [],
|
redirects: [],
|
||||||
refreshToken: "",
|
refreshToken: "",
|
||||||
registry: null,
|
registry: null,
|
||||||
@@ -118,6 +132,7 @@ const baseApp: ApplicationNested = {
|
|||||||
updateConfigSwarm: null,
|
updateConfigSwarm: null,
|
||||||
username: null,
|
username: null,
|
||||||
dockerContextPath: null,
|
dockerContextPath: null,
|
||||||
|
rollbackActive: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("unzipDrop using real zip files", () => {
|
describe("unzipDrop using real zip files", () => {
|
||||||
@@ -137,7 +152,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 });
|
||||||
@@ -147,67 +162,68 @@ describe("unzipDrop using real zip files", () => {
|
|||||||
} finally {
|
} finally {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should correctly extract a zip with a single root folder and a subfolder", async () => {
|
|
||||||
baseApp.appName = "folderwithfile";
|
|
||||||
// const appName = "folderwithfile";
|
|
||||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
|
||||||
const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
|
|
||||||
|
|
||||||
const zipBuffer = zip.toBuffer();
|
|
||||||
const file = new File([zipBuffer], "single.zip");
|
|
||||||
await unzipDrop(file, baseApp);
|
|
||||||
|
|
||||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
|
||||||
expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should correctly extract a zip with multiple root folders", async () => {
|
|
||||||
baseApp.appName = "two-folders";
|
|
||||||
// const appName = "two-folders";
|
|
||||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
|
||||||
const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
|
|
||||||
|
|
||||||
const zipBuffer = zip.toBuffer();
|
|
||||||
const file = new File([zipBuffer], "single.zip");
|
|
||||||
await unzipDrop(file, baseApp);
|
|
||||||
|
|
||||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
|
||||||
|
|
||||||
expect(files.some((f) => f.name === "folder1")).toBe(true);
|
|
||||||
expect(files.some((f) => f.name === "folder2")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should correctly extract a zip with a single root with a file", async () => {
|
|
||||||
baseApp.appName = "nested";
|
|
||||||
// const appName = "nested";
|
|
||||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
|
||||||
const zip = new AdmZip("./__test__/drop/zips/nested.zip");
|
|
||||||
|
|
||||||
const zipBuffer = zip.toBuffer();
|
|
||||||
const file = new File([zipBuffer], "single.zip");
|
|
||||||
await unzipDrop(file, baseApp);
|
|
||||||
|
|
||||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
|
||||||
|
|
||||||
expect(files.some((f) => f.name === "folder1")).toBe(true);
|
|
||||||
expect(files.some((f) => f.name === "folder2")).toBe(true);
|
|
||||||
expect(files.some((f) => f.name === "folder3")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should correctly extract a zip with a single root with a folder", async () => {
|
|
||||||
baseApp.appName = "folder-with-sibling-file";
|
|
||||||
// const appName = "folder-with-sibling-file";
|
|
||||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
|
||||||
const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
|
|
||||||
|
|
||||||
const zipBuffer = zip.toBuffer();
|
|
||||||
const file = new File([zipBuffer], "single.zip");
|
|
||||||
await unzipDrop(file, baseApp);
|
|
||||||
|
|
||||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
|
||||||
|
|
||||||
expect(files.some((f) => f.name === "folder1")).toBe(true);
|
|
||||||
expect(files.some((f) => f.name === "test.txt")).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// it("should correctly extract a zip with a single root folder and a subfolder", async () => {
|
||||||
|
// baseApp.appName = "folderwithfile";
|
||||||
|
// // const appName = "folderwithfile";
|
||||||
|
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||||
|
// const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
|
||||||
|
|
||||||
|
// const zipBuffer = zip.toBuffer();
|
||||||
|
// const file = new File([zipBuffer], "single.zip");
|
||||||
|
// await unzipDrop(file, baseApp);
|
||||||
|
|
||||||
|
// const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
|
// expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// it("should correctly extract a zip with multiple root folders", async () => {
|
||||||
|
// baseApp.appName = "two-folders";
|
||||||
|
// // const appName = "two-folders";
|
||||||
|
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||||
|
// const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
|
||||||
|
|
||||||
|
// const zipBuffer = zip.toBuffer();
|
||||||
|
// const file = new File([zipBuffer], "single.zip");
|
||||||
|
// await unzipDrop(file, baseApp);
|
||||||
|
|
||||||
|
// const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
|
|
||||||
|
// expect(files.some((f) => f.name === "folder1")).toBe(true);
|
||||||
|
// expect(files.some((f) => f.name === "folder2")).toBe(true);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// it("should correctly extract a zip with a single root with a file", async () => {
|
||||||
|
// baseApp.appName = "nested";
|
||||||
|
// // const appName = "nested";
|
||||||
|
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||||
|
// const zip = new AdmZip("./__test__/drop/zips/nested.zip");
|
||||||
|
|
||||||
|
// const zipBuffer = zip.toBuffer();
|
||||||
|
// const file = new File([zipBuffer], "single.zip");
|
||||||
|
// await unzipDrop(file, baseApp);
|
||||||
|
|
||||||
|
// const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
|
|
||||||
|
// expect(files.some((f) => f.name === "folder1")).toBe(true);
|
||||||
|
// expect(files.some((f) => f.name === "folder2")).toBe(true);
|
||||||
|
// expect(files.some((f) => f.name === "folder3")).toBe(true);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// it("should correctly extract a zip with a single root with a folder", async () => {
|
||||||
|
// baseApp.appName = "folder-with-sibling-file";
|
||||||
|
// // const appName = "folder-with-sibling-file";
|
||||||
|
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||||
|
// const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
|
||||||
|
|
||||||
|
// const zipBuffer = zip.toBuffer();
|
||||||
|
// const file = new File([zipBuffer], "single.zip");
|
||||||
|
// await unzipDrop(file, baseApp);
|
||||||
|
|
||||||
|
// const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
|
|
||||||
|
// expect(files.some((f) => f.name === "folder1")).toBe(true);
|
||||||
|
// expect(files.some((f) => f.name === "test.txt")).toBe(true);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
335
apps/dokploy/__test__/env/environment.test.ts
vendored
Normal file
335
apps/dokploy/__test__/env/environment.test.ts
vendored
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
import { prepareEnvironmentVariables } from "@dokploy/server/index";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const projectEnv = `
|
||||||
|
ENVIRONMENT=staging
|
||||||
|
DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db
|
||||||
|
PORT=3000
|
||||||
|
`;
|
||||||
|
|
||||||
|
const environmentEnv = `
|
||||||
|
NODE_ENV=development
|
||||||
|
API_URL=https://api.dev.example.com
|
||||||
|
REDIS_URL=redis://localhost:6379
|
||||||
|
DATABASE_NAME=dev_database
|
||||||
|
SECRET_KEY=env-secret-123
|
||||||
|
`;
|
||||||
|
|
||||||
|
describe("prepareEnvironmentVariables (environment variables)", () => {
|
||||||
|
it("resolves environment variables correctly", () => {
|
||||||
|
const serviceWithEnvVars = `
|
||||||
|
NODE_ENV=\${{environment.NODE_ENV}}
|
||||||
|
API_URL=\${{environment.API_URL}}
|
||||||
|
SERVICE_PORT=4000
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariables(
|
||||||
|
serviceWithEnvVars,
|
||||||
|
"",
|
||||||
|
environmentEnv,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"NODE_ENV=development",
|
||||||
|
"API_URL=https://api.dev.example.com",
|
||||||
|
"SERVICE_PORT=4000",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves both project and environment variables", () => {
|
||||||
|
const serviceWithBoth = `
|
||||||
|
ENVIRONMENT=\${{project.ENVIRONMENT}}
|
||||||
|
NODE_ENV=\${{environment.NODE_ENV}}
|
||||||
|
API_URL=\${{environment.API_URL}}
|
||||||
|
DATABASE_URL=\${{project.DATABASE_URL}}
|
||||||
|
SERVICE_PORT=4000
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariables(
|
||||||
|
serviceWithBoth,
|
||||||
|
projectEnv,
|
||||||
|
environmentEnv,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"ENVIRONMENT=staging",
|
||||||
|
"NODE_ENV=development",
|
||||||
|
"API_URL=https://api.dev.example.com",
|
||||||
|
"DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db",
|
||||||
|
"SERVICE_PORT=4000",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles undefined environment variables", () => {
|
||||||
|
const serviceWithUndefined = `
|
||||||
|
UNDEFINED_VAR=\${{environment.UNDEFINED_VAR}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
prepareEnvironmentVariables(serviceWithUndefined, "", environmentEnv),
|
||||||
|
).toThrow("Invalid environment variable: environment.UNDEFINED_VAR");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows service variables to override environment variables", () => {
|
||||||
|
const serviceOverrideEnv = `
|
||||||
|
NODE_ENV=production
|
||||||
|
API_URL=\${{environment.API_URL}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariables(
|
||||||
|
serviceOverrideEnv,
|
||||||
|
"",
|
||||||
|
environmentEnv,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"NODE_ENV=production", // Overrides environment variable
|
||||||
|
"API_URL=https://api.dev.example.com",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves complex references with project, environment, and service variables", () => {
|
||||||
|
const complexServiceEnv = `
|
||||||
|
FULL_DATABASE_URL=\${{project.DATABASE_URL}}/\${{environment.DATABASE_NAME}}
|
||||||
|
API_ENDPOINT=\${{environment.API_URL}}/\${{project.ENVIRONMENT}}/api
|
||||||
|
SERVICE_NAME=my-service
|
||||||
|
COMPLEX_VAR=\${{SERVICE_NAME}}-\${{environment.NODE_ENV}}-\${{project.ENVIRONMENT}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariables(
|
||||||
|
complexServiceEnv,
|
||||||
|
projectEnv,
|
||||||
|
environmentEnv,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"FULL_DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db/dev_database",
|
||||||
|
"API_ENDPOINT=https://api.dev.example.com/staging/api",
|
||||||
|
"SERVICE_NAME=my-service",
|
||||||
|
"COMPLEX_VAR=my-service-development-staging",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles environment variables with special characters", () => {
|
||||||
|
const specialEnvVars = `
|
||||||
|
SPECIAL_URL=https://special.com
|
||||||
|
COMPLEX_KEY="key-with-@#$%^&*()"
|
||||||
|
JWT_SECRET="secret-with-spaces and symbols!@#"
|
||||||
|
`;
|
||||||
|
|
||||||
|
const serviceWithSpecial = `
|
||||||
|
FULL_URL=\${{environment.SPECIAL_URL}}/path?key=\${{environment.COMPLEX_KEY}}
|
||||||
|
AUTH_SECRET=\${{environment.JWT_SECRET}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariables(
|
||||||
|
serviceWithSpecial,
|
||||||
|
"",
|
||||||
|
specialEnvVars,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"FULL_URL=https://special.com/path?key=key-with-@#$%^&*()",
|
||||||
|
"AUTH_SECRET=secret-with-spaces and symbols!@#",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maintains precedence: service > environment > project", () => {
|
||||||
|
const conflictingProjectEnv = `
|
||||||
|
NODE_ENV=production-project
|
||||||
|
API_URL=https://project.api.com
|
||||||
|
DATABASE_NAME=project_db
|
||||||
|
`;
|
||||||
|
|
||||||
|
const conflictingEnvironmentEnv = `
|
||||||
|
NODE_ENV=development-environment
|
||||||
|
API_URL=https://environment.api.com
|
||||||
|
DATABASE_NAME=env_db
|
||||||
|
`;
|
||||||
|
|
||||||
|
const serviceWithConflicts = `
|
||||||
|
NODE_ENV=service-override
|
||||||
|
PROJECT_ENV=\${{project.NODE_ENV}}
|
||||||
|
ENV_VAR=\${{environment.API_URL}}
|
||||||
|
DB_NAME=\${{environment.DATABASE_NAME}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariables(
|
||||||
|
serviceWithConflicts,
|
||||||
|
conflictingProjectEnv,
|
||||||
|
conflictingEnvironmentEnv,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"NODE_ENV=service-override", // Service wins
|
||||||
|
"PROJECT_ENV=production-project", // Project reference
|
||||||
|
"ENV_VAR=https://environment.api.com", // Environment reference
|
||||||
|
"DB_NAME=env_db", // Environment reference
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles empty environment variables", () => {
|
||||||
|
const serviceWithEmpty = `
|
||||||
|
SERVICE_VAR=test
|
||||||
|
PROJECT_VAR=\${{project.ENVIRONMENT}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariables(
|
||||||
|
serviceWithEmpty,
|
||||||
|
projectEnv,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved).toEqual(["SERVICE_VAR=test", "PROJECT_VAR=staging"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles mixed quotes and environment variables", () => {
|
||||||
|
const envWithQuotes = `
|
||||||
|
QUOTED_VAR="development"
|
||||||
|
SINGLE_QUOTED='https://api.dev.example.com'
|
||||||
|
MIXED_VAR="value with 'single' quotes"
|
||||||
|
`;
|
||||||
|
|
||||||
|
const serviceWithQuotes = `
|
||||||
|
NODE_ENV=\${{environment.QUOTED_VAR}}
|
||||||
|
API_URL=\${{environment.SINGLE_QUOTED}}
|
||||||
|
COMPLEX="Prefix-\${{environment.MIXED_VAR}}-Suffix"
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariables(
|
||||||
|
serviceWithQuotes,
|
||||||
|
"",
|
||||||
|
envWithQuotes,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"NODE_ENV=development",
|
||||||
|
"API_URL=https://api.dev.example.com",
|
||||||
|
"COMPLEX=Prefix-value with 'single' quotes-Suffix",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves multiple environment references in single value", () => {
|
||||||
|
const multiRefEnv = `
|
||||||
|
HOST=localhost
|
||||||
|
PORT=5432
|
||||||
|
USERNAME=postgres
|
||||||
|
PASSWORD=secret123
|
||||||
|
`;
|
||||||
|
|
||||||
|
const serviceWithMultiRefs = `
|
||||||
|
DATABASE_URL=postgresql://\${{environment.USERNAME}}:\${{environment.PASSWORD}}@\${{environment.HOST}}:\${{environment.PORT}}/mydb
|
||||||
|
CONNECTION_STRING=\${{environment.HOST}}:\${{environment.PORT}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariables(
|
||||||
|
serviceWithMultiRefs,
|
||||||
|
"",
|
||||||
|
multiRefEnv,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"DATABASE_URL=postgresql://postgres:secret123@localhost:5432/mydb",
|
||||||
|
"CONNECTION_STRING=localhost:5432",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles nested references with environment and project variables", () => {
|
||||||
|
const nestedProjectEnv = `
|
||||||
|
BASE_DOMAIN=example.com
|
||||||
|
PROTOCOL=https
|
||||||
|
`;
|
||||||
|
|
||||||
|
const nestedEnvironmentEnv = `
|
||||||
|
SUBDOMAIN=api.dev
|
||||||
|
PATH_PREFIX=/v1
|
||||||
|
`;
|
||||||
|
|
||||||
|
const serviceWithNested = `
|
||||||
|
FULL_URL=\${{project.PROTOCOL}}://\${{environment.SUBDOMAIN}}.\${{project.BASE_DOMAIN}}\${{environment.PATH_PREFIX}}/endpoint
|
||||||
|
API_BASE=\${{project.PROTOCOL}}://\${{environment.SUBDOMAIN}}.\${{project.BASE_DOMAIN}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariables(
|
||||||
|
serviceWithNested,
|
||||||
|
nestedProjectEnv,
|
||||||
|
nestedEnvironmentEnv,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"FULL_URL=https://api.dev.example.com/v1/endpoint",
|
||||||
|
"API_BASE=https://api.dev.example.com",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws error for malformed environment variable references", () => {
|
||||||
|
const serviceWithMalformed = `
|
||||||
|
MALFORMED1=\${{environment.}}
|
||||||
|
MALFORMED2=\${{environment}}
|
||||||
|
VALID=\${{environment.NODE_ENV}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Should throw error for empty variable name after environment.
|
||||||
|
expect(() =>
|
||||||
|
prepareEnvironmentVariables(serviceWithMalformed, "", environmentEnv),
|
||||||
|
).toThrow("Invalid environment variable: environment.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles environment variables with numeric values", () => {
|
||||||
|
const numericEnv = `
|
||||||
|
PORT=8080
|
||||||
|
TIMEOUT=30
|
||||||
|
RETRY_COUNT=3
|
||||||
|
PERCENTAGE=99.5
|
||||||
|
`;
|
||||||
|
|
||||||
|
const serviceWithNumeric = `
|
||||||
|
SERVER_PORT=\${{environment.PORT}}
|
||||||
|
REQUEST_TIMEOUT=\${{environment.TIMEOUT}}
|
||||||
|
MAX_RETRIES=\${{environment.RETRY_COUNT}}
|
||||||
|
SUCCESS_RATE=\${{environment.PERCENTAGE}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariables(
|
||||||
|
serviceWithNumeric,
|
||||||
|
"",
|
||||||
|
numericEnv,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"SERVER_PORT=8080",
|
||||||
|
"REQUEST_TIMEOUT=30",
|
||||||
|
"MAX_RETRIES=3",
|
||||||
|
"SUCCESS_RATE=99.5",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles boolean-like environment variables", () => {
|
||||||
|
const booleanEnv = `
|
||||||
|
DEBUG=true
|
||||||
|
ENABLED=false
|
||||||
|
PRODUCTION=1
|
||||||
|
DEVELOPMENT=0
|
||||||
|
`;
|
||||||
|
|
||||||
|
const serviceWithBoolean = `
|
||||||
|
DEBUG_MODE=\${{environment.DEBUG}}
|
||||||
|
FEATURE_ENABLED=\${{environment.ENABLED}}
|
||||||
|
IS_PROD=\${{environment.PRODUCTION}}
|
||||||
|
IS_DEV=\${{environment.DEVELOPMENT}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariables(
|
||||||
|
serviceWithBoolean,
|
||||||
|
"",
|
||||||
|
booleanEnv,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"DEBUG_MODE=true",
|
||||||
|
"FEATURE_ENABLED=false",
|
||||||
|
"IS_PROD=1",
|
||||||
|
"IS_DEV=0",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
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", () => {
|
||||||
|
|||||||
@@ -51,6 +51,35 @@ describe("processTemplate", () => {
|
|||||||
expect(result.domains).toHaveLength(0);
|
expect(result.domains).toHaveLength(0);
|
||||||
expect(result.mounts).toHaveLength(0);
|
expect(result.mounts).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should allow creation of real jwt secret", () => {
|
||||||
|
const template: CompleteTemplate = {
|
||||||
|
metadata: {} as any,
|
||||||
|
variables: {
|
||||||
|
jwt_secret: "cQsdycq1hDLopQonF6jUTqgQc5WEZTwWLL02J6XJ",
|
||||||
|
anon_payload: JSON.stringify({
|
||||||
|
role: "tester",
|
||||||
|
iss: "dockploy",
|
||||||
|
iat: "${timestamps:2025-01-01T00:00:00Z}",
|
||||||
|
exp: "${timestamps:2030-01-01T00:00:00Z}",
|
||||||
|
}),
|
||||||
|
anon_key: "${jwt:jwt_secret:anon_payload}",
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
domains: [],
|
||||||
|
env: {
|
||||||
|
ANON_KEY: "${anon_key}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = processTemplate(template, mockSchema);
|
||||||
|
expect(result.envs).toHaveLength(1);
|
||||||
|
expect(result.envs).toContain(
|
||||||
|
"ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNzM1Njg5NjAwIiwiZXhwIjoiMTg5MzQ1NjAwMCIsInJvbGUiOiJ0ZXN0ZXIiLCJpc3MiOiJkb2NrcGxveSJ9.BG5JoxL2_NaTFbPgyZdm3kRWenf_O3su_HIRKGCJ_kY",
|
||||||
|
);
|
||||||
|
expect(result.mounts).toHaveLength(0);
|
||||||
|
expect(result.domains).toHaveLength(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("domains processing", () => {
|
describe("domains processing", () => {
|
||||||
|
|||||||
232
apps/dokploy/__test__/templates/helpers.template.test.ts
Normal file
232
apps/dokploy/__test__/templates/helpers.template.test.ts
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
import type { Schema } from "@dokploy/server/templates";
|
||||||
|
import { processValue } from "@dokploy/server/templates/processors";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe("helpers functions", () => {
|
||||||
|
// Mock schema for testing
|
||||||
|
const mockSchema: Schema = {
|
||||||
|
projectName: "test",
|
||||||
|
serverIp: "127.0.0.1",
|
||||||
|
};
|
||||||
|
// some helpers to test jwt
|
||||||
|
type JWTParts = [string, string, string];
|
||||||
|
const jwtMatchExp = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/;
|
||||||
|
const jwtBase64Decode = (str: string) => {
|
||||||
|
const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
const padding = "=".repeat((4 - (base64.length % 4)) % 4);
|
||||||
|
const decoded = Buffer.from(base64 + padding, "base64").toString("utf-8");
|
||||||
|
return JSON.parse(decoded);
|
||||||
|
};
|
||||||
|
const jwtCheckHeader = (jwtHeader: string) => {
|
||||||
|
const decodedHeader = jwtBase64Decode(jwtHeader);
|
||||||
|
expect(decodedHeader).toHaveProperty("alg");
|
||||||
|
expect(decodedHeader).toHaveProperty("typ");
|
||||||
|
expect(decodedHeader.alg).toEqual("HS256");
|
||||||
|
expect(decodedHeader.typ).toEqual("JWT");
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("${domain}", () => {
|
||||||
|
it("should generate a random domain", () => {
|
||||||
|
const domain = processValue("${domain}", {}, mockSchema);
|
||||||
|
expect(domain.startsWith(`${mockSchema.projectName}-`)).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
domain.endsWith(
|
||||||
|
`${mockSchema.serverIp.replaceAll(".", "-")}.traefik.me`,
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${base64}", () => {
|
||||||
|
it("should generate a base64 string", () => {
|
||||||
|
const base64 = processValue("${base64}", {}, mockSchema);
|
||||||
|
expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/);
|
||||||
|
});
|
||||||
|
it.each([
|
||||||
|
[4, 8],
|
||||||
|
[8, 12],
|
||||||
|
[16, 24],
|
||||||
|
[32, 44],
|
||||||
|
[64, 88],
|
||||||
|
[128, 172],
|
||||||
|
])(
|
||||||
|
"should generate a base64 string from parameter %d bytes length",
|
||||||
|
(length, finalLength) => {
|
||||||
|
const base64 = processValue(`\${base64:${length}}`, {}, mockSchema);
|
||||||
|
expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/);
|
||||||
|
expect(base64.length).toBe(finalLength);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${password}", () => {
|
||||||
|
it("should generate a password string", () => {
|
||||||
|
const password = processValue("${password}", {}, mockSchema);
|
||||||
|
expect(password).toMatch(/^[A-Za-z0-9]+$/);
|
||||||
|
});
|
||||||
|
it.each([6, 8, 12, 16, 32])(
|
||||||
|
"should generate a password string respecting parameter %d length",
|
||||||
|
(length) => {
|
||||||
|
const password = processValue(`\${password:${length}}`, {}, mockSchema);
|
||||||
|
expect(password).toMatch(/^[A-Za-z0-9]+$/);
|
||||||
|
expect(password.length).toBe(length);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${hash}", () => {
|
||||||
|
it("should generate a hash string", () => {
|
||||||
|
const hash = processValue("${hash}", {}, mockSchema);
|
||||||
|
expect(hash).toMatch(/^[A-Za-z0-9]+$/);
|
||||||
|
});
|
||||||
|
it.each([6, 8, 12, 16, 32])(
|
||||||
|
"should generate a hash string respecting parameter %d length",
|
||||||
|
(length) => {
|
||||||
|
const hash = processValue(`\${hash:${length}}`, {}, mockSchema);
|
||||||
|
expect(hash).toMatch(/^[A-Za-z0-9]+$/);
|
||||||
|
expect(hash.length).toBe(length);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${uuid}", () => {
|
||||||
|
it("should generate a UUID string", () => {
|
||||||
|
const uuid = processValue("${uuid}", {}, mockSchema);
|
||||||
|
expect(uuid).toMatch(
|
||||||
|
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${timestamp}", () => {
|
||||||
|
it("should generate a timestamp string in milliseconds", () => {
|
||||||
|
const timestamp = processValue("${timestamp}", {}, mockSchema);
|
||||||
|
const nowLength = Math.floor(Date.now()).toString().length;
|
||||||
|
expect(timestamp).toMatch(/^\d+$/);
|
||||||
|
expect(timestamp.length).toBe(nowLength);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("${timestampms}", () => {
|
||||||
|
it("should generate a timestamp string in milliseconds", () => {
|
||||||
|
const timestamp = processValue("${timestampms}", {}, mockSchema);
|
||||||
|
const nowLength = Date.now().toString().length;
|
||||||
|
expect(timestamp).toMatch(/^\d+$/);
|
||||||
|
expect(timestamp.length).toBe(nowLength);
|
||||||
|
});
|
||||||
|
it("should generate a timestamp string in milliseconds from parameter", () => {
|
||||||
|
const timestamp = processValue(
|
||||||
|
"${timestampms:2025-01-01}",
|
||||||
|
{},
|
||||||
|
mockSchema,
|
||||||
|
);
|
||||||
|
expect(timestamp).toEqual("1735689600000");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("${timestamps}", () => {
|
||||||
|
it("should generate a timestamp string in seconds", () => {
|
||||||
|
const timestamps = processValue("${timestamps}", {}, mockSchema);
|
||||||
|
const nowLength = Math.floor(Date.now() / 1000).toString().length;
|
||||||
|
expect(timestamps).toMatch(/^\d+$/);
|
||||||
|
expect(timestamps.length).toBe(nowLength);
|
||||||
|
});
|
||||||
|
it("should generate a timestamp string in seconds from parameter", () => {
|
||||||
|
const timestamps = processValue(
|
||||||
|
"${timestamps:2025-01-01}",
|
||||||
|
{},
|
||||||
|
mockSchema,
|
||||||
|
);
|
||||||
|
expect(timestamps).toEqual("1735689600");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${randomPort}", () => {
|
||||||
|
it("should generate a random port string", () => {
|
||||||
|
const randomPort = processValue("${randomPort}", {}, mockSchema);
|
||||||
|
expect(randomPort).toMatch(/^\d+$/);
|
||||||
|
expect(Number(randomPort)).toBeLessThan(65536);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${username}", () => {
|
||||||
|
it("should generate a username string", () => {
|
||||||
|
const username = processValue("${username}", {}, mockSchema);
|
||||||
|
expect(username).toMatch(/^[a-zA-Z0-9._-]{3,}$/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${email}", () => {
|
||||||
|
it("should generate an email string", () => {
|
||||||
|
const email = processValue("${email}", {}, mockSchema);
|
||||||
|
expect(email).toMatch(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${jwt}", () => {
|
||||||
|
it("should generate a JWT string", () => {
|
||||||
|
const jwt = processValue("${jwt}", {}, mockSchema);
|
||||||
|
expect(jwt).toMatch(jwtMatchExp);
|
||||||
|
const parts = jwt.split(".") as JWTParts;
|
||||||
|
const decodedPayload = jwtBase64Decode(parts[1]);
|
||||||
|
jwtCheckHeader(parts[0]);
|
||||||
|
expect(decodedPayload).toHaveProperty("iat");
|
||||||
|
expect(decodedPayload).toHaveProperty("iss");
|
||||||
|
expect(decodedPayload).toHaveProperty("exp");
|
||||||
|
expect(decodedPayload.iss).toEqual("dokploy");
|
||||||
|
});
|
||||||
|
it.each([6, 8, 12, 16, 32])(
|
||||||
|
"should generate a random hex string from parameter %d byte length",
|
||||||
|
(length) => {
|
||||||
|
const jwt = processValue(`\${jwt:${length}}`, {}, mockSchema);
|
||||||
|
expect(jwt).toMatch(/^[A-Za-z0-9-_.]+$/);
|
||||||
|
expect(jwt.length).toBeGreaterThanOrEqual(length); // bytes translated to hex can take up to 2x the length
|
||||||
|
expect(jwt.length).toBeLessThanOrEqual(length * 2);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
describe("${jwt:secret}", () => {
|
||||||
|
it("should generate a JWT string respecting parameter secret from variable", () => {
|
||||||
|
const jwt = processValue(
|
||||||
|
"${jwt:secret}",
|
||||||
|
{ secret: "mysecret" },
|
||||||
|
mockSchema,
|
||||||
|
);
|
||||||
|
expect(jwt).toMatch(jwtMatchExp);
|
||||||
|
const parts = jwt.split(".") as JWTParts;
|
||||||
|
const decodedPayload = jwtBase64Decode(parts[1]);
|
||||||
|
jwtCheckHeader(parts[0]);
|
||||||
|
expect(decodedPayload).toHaveProperty("iat");
|
||||||
|
expect(decodedPayload).toHaveProperty("iss");
|
||||||
|
expect(decodedPayload).toHaveProperty("exp");
|
||||||
|
expect(decodedPayload.iss).toEqual("dokploy");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("${jwt:secret:payload}", () => {
|
||||||
|
it("should generate a JWT string respecting parameters secret and payload from variables", () => {
|
||||||
|
const iat = Math.floor(new Date("2025-01-01T00:00:00Z").getTime() / 1000);
|
||||||
|
const expiry = iat + 3600;
|
||||||
|
const jwt = processValue(
|
||||||
|
"${jwt:secret:payload}",
|
||||||
|
{
|
||||||
|
secret: "mysecret",
|
||||||
|
payload: `{"iss": "test-issuer", "iat": ${iat}, "exp": ${expiry}, "customprop": "customvalue"}`,
|
||||||
|
},
|
||||||
|
mockSchema,
|
||||||
|
);
|
||||||
|
expect(jwt).toMatch(jwtMatchExp);
|
||||||
|
const parts = jwt.split(".") as JWTParts;
|
||||||
|
jwtCheckHeader(parts[0]);
|
||||||
|
const decodedPayload = jwtBase64Decode(parts[1]);
|
||||||
|
expect(decodedPayload).toHaveProperty("iat");
|
||||||
|
expect(decodedPayload.iat).toEqual(iat);
|
||||||
|
expect(decodedPayload).toHaveProperty("iss");
|
||||||
|
expect(decodedPayload.iss).toEqual("test-issuer");
|
||||||
|
expect(decodedPayload).toHaveProperty("exp");
|
||||||
|
expect(decodedPayload.exp).toEqual(expiry);
|
||||||
|
expect(decodedPayload).toHaveProperty("customprop");
|
||||||
|
expect(decodedPayload.customprop).toEqual("customvalue");
|
||||||
|
expect(jwt).toEqual(
|
||||||
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -16,6 +16,8 @@ import { beforeEach, expect, test, vi } from "vitest";
|
|||||||
const baseAdmin: User = {
|
const baseAdmin: User = {
|
||||||
https: false,
|
https: false,
|
||||||
enablePaidFeatures: false,
|
enablePaidFeatures: false,
|
||||||
|
allowImpersonation: false,
|
||||||
|
role: "user",
|
||||||
metricsConfig: {
|
metricsConfig: {
|
||||||
containers: {
|
containers: {
|
||||||
refreshRate: 20,
|
refreshRate: 20,
|
||||||
|
|||||||
@@ -1,11 +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,
|
||||||
applicationId: "",
|
applicationId: "",
|
||||||
|
previewLabels: [],
|
||||||
herokuVersion: "",
|
herokuVersion: "",
|
||||||
giteaRepository: "",
|
giteaRepository: "",
|
||||||
giteaOwner: "",
|
giteaOwner: "",
|
||||||
@@ -16,6 +17,8 @@ const baseApp: ApplicationNested = {
|
|||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
appName: "",
|
appName: "",
|
||||||
autoDeploy: true,
|
autoDeploy: true,
|
||||||
|
enableSubmodules: false,
|
||||||
|
previewRequireCollaboratorPermissions: false,
|
||||||
serverId: "",
|
serverId: "",
|
||||||
branch: null,
|
branch: null,
|
||||||
dockerBuildStage: "",
|
dockerBuildStage: "",
|
||||||
@@ -24,6 +27,7 @@ const baseApp: ApplicationNested = {
|
|||||||
buildArgs: null,
|
buildArgs: null,
|
||||||
isPreviewDeploymentsActive: false,
|
isPreviewDeploymentsActive: false,
|
||||||
previewBuildArgs: null,
|
previewBuildArgs: null,
|
||||||
|
triggerType: "push",
|
||||||
previewCertificateType: "none",
|
previewCertificateType: "none",
|
||||||
previewEnv: null,
|
previewEnv: null,
|
||||||
previewHttps: false,
|
previewHttps: false,
|
||||||
@@ -32,13 +36,22 @@ const baseApp: ApplicationNested = {
|
|||||||
previewLimit: 0,
|
previewLimit: 0,
|
||||||
previewCustomCertResolver: null,
|
previewCustomCertResolver: null,
|
||||||
previewWildcard: "",
|
previewWildcard: "",
|
||||||
project: {
|
environmentId: "",
|
||||||
|
environment: {
|
||||||
env: "",
|
env: "",
|
||||||
organizationId: "",
|
environmentId: "",
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
|
||||||
createdAt: "",
|
createdAt: "",
|
||||||
|
description: "",
|
||||||
projectId: "",
|
projectId: "",
|
||||||
|
project: {
|
||||||
|
env: "",
|
||||||
|
organizationId: "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
createdAt: "",
|
||||||
|
projectId: "",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
buildPath: "/",
|
buildPath: "/",
|
||||||
gitlabPathNamespace: "",
|
gitlabPathNamespace: "",
|
||||||
@@ -81,8 +94,8 @@ const baseApp: ApplicationNested = {
|
|||||||
password: null,
|
password: null,
|
||||||
placementSwarm: null,
|
placementSwarm: null,
|
||||||
ports: [],
|
ports: [],
|
||||||
projectId: "",
|
|
||||||
publishDirectory: null,
|
publishDirectory: null,
|
||||||
|
isStaticSpa: null,
|
||||||
redirects: [],
|
redirects: [],
|
||||||
refreshToken: "",
|
refreshToken: "",
|
||||||
registry: null,
|
registry: null,
|
||||||
@@ -115,6 +128,8 @@ const baseDomain: Domain = {
|
|||||||
domainType: "application",
|
domainType: "application",
|
||||||
uniqueConfigKey: 1,
|
uniqueConfigKey: 1,
|
||||||
previewDeploymentId: "",
|
previewDeploymentId: "",
|
||||||
|
internalPath: "/",
|
||||||
|
stripPath: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseRedirect: Redirect = {
|
const baseRedirect: Redirect = {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "vitest";
|
|
||||||
import { normalizeS3Path } from "@dokploy/server/utils/backups/utils";
|
import { normalizeS3Path } from "@dokploy/server/utils/backups/utils";
|
||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
describe("normalizeS3Path", () => {
|
describe("normalizeS3Path", () => {
|
||||||
test("should handle empty and whitespace-only prefix", () => {
|
test("should handle empty and whitespace-only prefix", () => {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -263,7 +263,7 @@ export const ShowImport = ({ composeId }: Props) => {
|
|||||||
{templateInfo.template.envs.map((env, index) => (
|
{templateInfo.template.envs.map((env, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="rounded-lg border bg-card p-2 font-mono text-sm"
|
className="rounded-lg truncate border bg-card p-2 font-mono text-sm"
|
||||||
>
|
>
|
||||||
{env}
|
{env}
|
||||||
</div>
|
</div>
|
||||||
@@ -328,7 +328,7 @@ export const ShowImport = ({ composeId }: Props) => {
|
|||||||
<DialogDescription>Mount File Content</DialogDescription>
|
<DialogDescription>Mount File Content</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<ScrollArea className="h-[25vh] pr-4">
|
<ScrollArea className="h-[45vh] pr-4">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
language="yaml"
|
language="yaml"
|
||||||
value={selectedMount?.content || ""}
|
value={selectedMount?.content || ""}
|
||||||
|
|||||||
@@ -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 { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { parse, stringify, YAMLParseError } from "yaml";
|
||||||
|
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(),
|
||||||
@@ -38,11 +38,11 @@ interface Props {
|
|||||||
|
|
||||||
export const validateAndFormatYAML = (yamlText: string) => {
|
export const validateAndFormatYAML = (yamlText: string) => {
|
||||||
try {
|
try {
|
||||||
const obj = jsyaml.load(yamlText);
|
const obj = parse(yamlText);
|
||||||
const formattedYaml = jsyaml.dump(obj, { indent: 4 });
|
const formattedYaml = stringify(obj, { indent: 4 });
|
||||||
return { valid: true, formattedYaml, error: null };
|
return { valid: true, formattedYaml, error: null };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof jsyaml.YAMLException) {
|
if (error instanceof YAMLParseError) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
formattedYaml: yamlText,
|
formattedYaml: yamlText,
|
||||||
@@ -89,7 +89,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
|||||||
if (!valid) {
|
if (!valid) {
|
||||||
form.setError("traefikConfig", {
|
form.setError("traefikConfig", {
|
||||||
type: "manual",
|
type: "manual",
|
||||||
message: error || "Invalid YAML",
|
message: (error as string) || "Invalid YAML",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -247,7 +247,7 @@ export const UpdateVolume = ({
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="content"
|
name="content"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem className="max-w-full max-w-[45rem]">
|
||||||
<FormLabel>Content</FormLabel>
|
<FormLabel>Content</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -256,7 +256,7 @@ export const UpdateVolume = ({
|
|||||||
placeholder={`NODE_ENV=production
|
placeholder={`NODE_ENV=production
|
||||||
PORT=3000
|
PORT=3000
|
||||||
`}
|
`}
|
||||||
className="h-96 font-mono"
|
className="h-96 font-mono w-full"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
|
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 { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -13,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",
|
||||||
@@ -62,10 +64,12 @@ const mySchema = z.discriminatedUnion("buildType", [
|
|||||||
publishDirectory: z.string().optional(),
|
publishDirectory: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
buildType: z.literal(BuildType.static),
|
buildType: z.literal(BuildType.railpack),
|
||||||
|
railpackVersion: z.string().nullable().default("0.2.2"),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
buildType: z.literal(BuildType.railpack),
|
buildType: z.literal(BuildType.static),
|
||||||
|
isStaticSpa: z.boolean().default(false),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -82,6 +86,8 @@ interface ApplicationData {
|
|||||||
dockerBuildStage?: string | null;
|
dockerBuildStage?: string | null;
|
||||||
herokuVersion?: string | null;
|
herokuVersion?: string | null;
|
||||||
publishDirectory?: string | null;
|
publishDirectory?: string | null;
|
||||||
|
isStaticSpa?: boolean | null;
|
||||||
|
railpackVersion?: string | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidBuildType(value: string): value is BuildType {
|
function isValidBuildType(value: string): value is BuildType {
|
||||||
@@ -114,16 +120,19 @@ const resetData = (data: ApplicationData): AddTemplate => {
|
|||||||
case BuildType.static:
|
case BuildType.static:
|
||||||
return {
|
return {
|
||||||
buildType: BuildType.static,
|
buildType: BuildType.static,
|
||||||
|
isStaticSpa: data.isStaticSpa ?? false,
|
||||||
};
|
};
|
||||||
case BuildType.railpack:
|
case BuildType.railpack:
|
||||||
return {
|
return {
|
||||||
buildType: BuildType.railpack,
|
buildType: BuildType.railpack,
|
||||||
|
railpackVersion: data.railpackVersion || null,
|
||||||
};
|
};
|
||||||
default:
|
default: {
|
||||||
const buildType = data.buildType as BuildType;
|
const buildType = data.buildType as BuildType;
|
||||||
return {
|
return {
|
||||||
buildType,
|
buildType,
|
||||||
} as AddTemplate;
|
} as AddTemplate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -173,6 +182,12 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
data.buildType === BuildType.heroku_buildpacks
|
data.buildType === BuildType.heroku_buildpacks
|
||||||
? data.herokuVersion
|
? data.herokuVersion
|
||||||
: null,
|
: null,
|
||||||
|
isStaticSpa:
|
||||||
|
data.buildType === BuildType.static ? data.isStaticSpa : null,
|
||||||
|
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");
|
||||||
@@ -200,6 +215,22 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
|
<AlertBlock>
|
||||||
|
Builders can consume significant memory and CPU resources
|
||||||
|
(recommended: 4+ GB RAM and 2+ CPU cores). For production
|
||||||
|
environments, please review our{" "}
|
||||||
|
<a
|
||||||
|
href="https://docs.dokploy.com/docs/core/applications/going-production"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="font-medium underline underline-offset-4"
|
||||||
|
>
|
||||||
|
Production Guide
|
||||||
|
</a>{" "}
|
||||||
|
for best practices and optimization recommendations. Builders are
|
||||||
|
suitable for development and prototyping purposes when you have
|
||||||
|
sufficient resources available.
|
||||||
|
</AlertBlock>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-4 p-2"
|
className="grid w-full gap-4 p-2"
|
||||||
@@ -347,6 +378,49 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{buildType === BuildType.static && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="isStaticSpa"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex items-center gap-x-2 p-2">
|
||||||
|
<Checkbox
|
||||||
|
id="checkboxIsStaticSpa"
|
||||||
|
value={String(field.value)}
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
<FormLabel htmlFor="checkboxIsStaticSpa">
|
||||||
|
Single Page Application (SPA)
|
||||||
|
</FormLabel>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{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,15 +13,17 @@ 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 {
|
||||||
applicationId: string;
|
id: string;
|
||||||
|
type: "application" | "compose";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CancelQueues = ({ applicationId }: Props) => {
|
export const CancelQueues = ({ id, type }: Props) => {
|
||||||
const { mutateAsync, isLoading } = api.application.cleanQueues.useMutation();
|
const { mutateAsync, isLoading } =
|
||||||
|
type === "application"
|
||||||
|
? api.application.cleanQueues.useMutation()
|
||||||
|
: api.compose.cleanQueues.useMutation();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
if (isCloud) {
|
if (isCloud) {
|
||||||
@@ -48,7 +52,8 @@ export const CancelQueues = ({ applicationId }: Props) => {
|
|||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
applicationId,
|
applicationId: id || "",
|
||||||
|
composeId: id || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Queues are being cleaned");
|
toast.success("Queues are being cleaned");
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { RefreshCcw } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -10,14 +12,16 @@ 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 {
|
||||||
applicationId: string;
|
id: string;
|
||||||
|
type: "application" | "compose";
|
||||||
}
|
}
|
||||||
export const RefreshToken = ({ applicationId }: Props) => {
|
export const RefreshToken = ({ id, type }: Props) => {
|
||||||
const { mutateAsync } = api.application.refreshToken.useMutation();
|
const { mutateAsync } =
|
||||||
|
type === "application"
|
||||||
|
? api.application.refreshToken.useMutation()
|
||||||
|
: api.compose.refreshToken.useMutation();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
@@ -37,12 +41,19 @@ export const RefreshToken = ({ applicationId }: Props) => {
|
|||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
applicationId,
|
applicationId: id || "",
|
||||||
|
composeId: id || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
utils.application.one.invalidate({
|
if (type === "application") {
|
||||||
applicationId,
|
utils.application.one.invalidate({
|
||||||
});
|
applicationId: id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
utils.compose.one.invalidate({
|
||||||
|
composeId: id,
|
||||||
|
});
|
||||||
|
}
|
||||||
toast.success("Refresh updated");
|
toast.success("Refresh updated");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||||
|
import type { RouterOutputs } from "@/utils/api";
|
||||||
|
import { ShowDeployment } from "../deployments/show-deployment";
|
||||||
|
import { ShowDeployments } from "./show-deployments";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
type:
|
||||||
|
| "application"
|
||||||
|
| "compose"
|
||||||
|
| "schedule"
|
||||||
|
| "server"
|
||||||
|
| "backup"
|
||||||
|
| "previewDeployment"
|
||||||
|
| "volumeBackup";
|
||||||
|
serverId?: string;
|
||||||
|
refreshToken?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatDuration = (seconds: number) => {
|
||||||
|
if (seconds < 60) return `${seconds}s`;
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = seconds % 60;
|
||||||
|
return `${minutes}m ${remainingSeconds}s`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ShowDeploymentsModal = ({
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
serverId,
|
||||||
|
refreshToken,
|
||||||
|
children,
|
||||||
|
}: Props) => {
|
||||||
|
const [activeLog, setActiveLog] = useState<
|
||||||
|
RouterOutputs["deployment"]["all"][number] | null
|
||||||
|
>(null);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
{children ? (
|
||||||
|
children
|
||||||
|
) : (
|
||||||
|
<Button className="sm:w-auto w-full" size="sm" variant="outline">
|
||||||
|
View Logs
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-5xl p-0">
|
||||||
|
<ShowDeployments
|
||||||
|
id={id}
|
||||||
|
type={type}
|
||||||
|
serverId={serverId}
|
||||||
|
refreshToken={refreshToken}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<ShowDeployment
|
||||||
|
serverId={serverId || ""}
|
||||||
|
open={Boolean(activeLog && activeLog.logPath !== null)}
|
||||||
|
onClose={() => setActiveLog(null)}
|
||||||
|
logPath={activeLog?.logPath || ""}
|
||||||
|
errorMessage={activeLog?.errorMessage || ""}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
|
import { Clock, Loader2, RefreshCcw, RocketIcon, Settings } from "lucide-react";
|
||||||
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
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 { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -8,64 +14,202 @@ 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 { RocketIcon } 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";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
id: string;
|
||||||
|
type:
|
||||||
|
| "application"
|
||||||
|
| "compose"
|
||||||
|
| "schedule"
|
||||||
|
| "server"
|
||||||
|
| "backup"
|
||||||
|
| "previewDeployment"
|
||||||
|
| "volumeBackup";
|
||||||
|
refreshToken?: string;
|
||||||
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDeployments = ({ applicationId }: Props) => {
|
export const formatDuration = (seconds: number) => {
|
||||||
|
if (seconds < 60) return `${seconds}s`;
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = seconds % 60;
|
||||||
|
return `${minutes}m ${remainingSeconds}s`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ShowDeployments = ({
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
refreshToken,
|
||||||
|
serverId,
|
||||||
|
}: Props) => {
|
||||||
const [activeLog, setActiveLog] = useState<
|
const [activeLog, setActiveLog] = useState<
|
||||||
RouterOutputs["deployment"]["all"][number] | null
|
RouterOutputs["deployment"]["all"][number] | null
|
||||||
>(null);
|
>(null);
|
||||||
const { data } = api.application.one.useQuery({ applicationId });
|
const { data: deployments, isLoading: isLoadingDeployments } =
|
||||||
const { data: deployments } = api.deployment.all.useQuery(
|
api.deployment.allByType.useQuery(
|
||||||
{ applicationId },
|
{
|
||||||
{
|
id,
|
||||||
enabled: !!applicationId,
|
type,
|
||||||
refetchInterval: 1000,
|
},
|
||||||
},
|
{
|
||||||
);
|
enabled: !!id,
|
||||||
|
refetchInterval: 1000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
|
const { mutateAsync: rollback, isLoading: isRollingBack } =
|
||||||
|
api.rollback.rollback.useMutation();
|
||||||
|
const { mutateAsync: killProcess, isLoading: isKillingProcess } =
|
||||||
|
api.deployment.killProcess.useMutation();
|
||||||
|
|
||||||
|
// Cancel deployment mutations
|
||||||
|
const {
|
||||||
|
mutateAsync: cancelApplicationDeployment,
|
||||||
|
isLoading: isCancellingApp,
|
||||||
|
} = api.application.cancelDeployment.useMutation();
|
||||||
|
const {
|
||||||
|
mutateAsync: cancelComposeDeployment,
|
||||||
|
isLoading: isCancellingCompose,
|
||||||
|
} = api.compose.cancelDeployment.useMutation();
|
||||||
|
|
||||||
const [url, setUrl] = React.useState("");
|
const [url, setUrl] = React.useState("");
|
||||||
|
|
||||||
|
// Check for stuck deployment (more than 9 minutes) - only for the most recent deployment
|
||||||
|
const stuckDeployment = useMemo(() => {
|
||||||
|
if (!isCloud || !deployments || deployments.length === 0) return null;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const NINE_MINUTES = 10 * 60 * 1000; // 9 minutes in milliseconds
|
||||||
|
|
||||||
|
// Get the most recent deployment (first in the list since they're sorted by date)
|
||||||
|
const mostRecentDeployment = deployments[0];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!mostRecentDeployment ||
|
||||||
|
mostRecentDeployment.status !== "running" ||
|
||||||
|
!mostRecentDeployment.startedAt
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = new Date(mostRecentDeployment.startedAt).getTime();
|
||||||
|
const elapsed = now - startTime;
|
||||||
|
|
||||||
|
return elapsed > NINE_MINUTES ? mostRecentDeployment : null;
|
||||||
|
}, [isCloud, deployments]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUrl(document.location.origin);
|
setUrl(document.location.origin);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background border-none">
|
||||||
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<CardTitle className="text-xl">Deployments</CardTitle>
|
<CardTitle className="text-xl">Deployments</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
See all the 10 last deployments for this application
|
See the last 10 deployments for this {type}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<CancelQueues applicationId={applicationId} />
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
{(type === "application" || type === "compose") && (
|
||||||
|
<CancelQueues id={id} type={type} />
|
||||||
|
)}
|
||||||
|
{type === "application" && (
|
||||||
|
<ShowRollbackSettings applicationId={id}>
|
||||||
|
<Button variant="outline">
|
||||||
|
Configure Rollbacks <Settings className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</ShowRollbackSettings>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-2 text-sm">
|
{stuckDeployment && (type === "application" || type === "compose") && (
|
||||||
<span>
|
<AlertBlock
|
||||||
If you want to re-deploy this application use this URL in the config
|
type="warning"
|
||||||
of your git provider or docker
|
className="flex-col items-start w-full p-4"
|
||||||
</span>
|
>
|
||||||
<div className="flex flex-row items-center gap-2 flex-wrap">
|
<div className="flex flex-col gap-3">
|
||||||
<span>Webhook URL: </span>
|
<div>
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="font-medium text-sm mb-1">
|
||||||
<span className="break-all text-muted-foreground">
|
Build appears to be stuck
|
||||||
{`${url}/api/deploy/${data?.refreshToken}`}
|
</div>
|
||||||
</span>
|
<p className="text-sm">
|
||||||
<RefreshToken applicationId={applicationId} />
|
Hey! Looks like the build has been running for more than 10
|
||||||
|
minutes. Would you like to cancel this deployment?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
className="w-fit"
|
||||||
|
isLoading={
|
||||||
|
type === "application" ? isCancellingApp : isCancellingCompose
|
||||||
|
}
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
if (type === "application") {
|
||||||
|
await cancelApplicationDeployment({
|
||||||
|
applicationId: id,
|
||||||
|
});
|
||||||
|
} else if (type === "compose") {
|
||||||
|
await cancelComposeDeployment({
|
||||||
|
composeId: id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
toast.success("Deployment cancellation requested");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to cancel deployment",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel Deployment
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
{refreshToken && (
|
||||||
|
<div className="flex flex-col gap-2 text-sm">
|
||||||
|
<span>
|
||||||
|
If you want to re-deploy this application use this URL in the
|
||||||
|
config of your git provider or docker
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-row items-center gap-2 flex-wrap">
|
||||||
|
<span>Webhook URL: </span>
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
<span className="break-all text-muted-foreground">
|
||||||
|
{`${url}/api/deploy${
|
||||||
|
type === "compose" ? "/compose" : ""
|
||||||
|
}/${refreshToken}`}
|
||||||
|
</span>
|
||||||
|
{(type === "application" || type === "compose") && (
|
||||||
|
<RefreshToken id={id} type={type} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
{data?.deployments?.length === 0 ? (
|
|
||||||
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
{isLoadingDeployments ? (
|
||||||
|
<div className="flex w-full flex-row items-center justify-center gap-3 pt-10 min-h-[25vh]">
|
||||||
|
<Loader2 className="size-6 text-muted-foreground animate-spin" />
|
||||||
|
<span className="text-base text-muted-foreground">
|
||||||
|
Loading deployments...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : deployments?.length === 0 ? (
|
||||||
|
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10 min-h-[25vh]">
|
||||||
<RocketIcon className="size-8 text-muted-foreground" />
|
<RocketIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No deployments found
|
No deployments found
|
||||||
@@ -96,24 +240,99 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-end gap-2">
|
<div className="flex flex-col items-end gap-2">
|
||||||
<div className="text-sm capitalize text-muted-foreground">
|
<div className="text-sm capitalize text-muted-foreground flex items-center gap-2">
|
||||||
<DateTooltip date={deployment.createdAt} />
|
<DateTooltip date={deployment.createdAt} />
|
||||||
|
{deployment.startedAt && deployment.finishedAt && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-[10px] gap-1 flex items-center"
|
||||||
|
>
|
||||||
|
<Clock className="size-3" />
|
||||||
|
{formatDuration(
|
||||||
|
Math.floor(
|
||||||
|
(new Date(deployment.finishedAt).getTime() -
|
||||||
|
new Date(deployment.startedAt).getTime()) /
|
||||||
|
1000,
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<div className="flex flex-row items-center gap-2">
|
||||||
onClick={() => {
|
{deployment.pid && deployment.status === "running" && (
|
||||||
setActiveLog(deployment);
|
<DialogAction
|
||||||
}}
|
title="Kill Process"
|
||||||
>
|
description="Are you sure you want to kill the process?"
|
||||||
View
|
type="default"
|
||||||
</Button>
|
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
|
||||||
|
onClick={() => {
|
||||||
|
setActiveLog(deployment);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{deployment?.rollback &&
|
||||||
|
deployment.status === "done" &&
|
||||||
|
type === "application" && (
|
||||||
|
<DialogAction
|
||||||
|
title="Rollback to this deployment"
|
||||||
|
description="Are you sure you want to rollback to this deployment?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await rollback({
|
||||||
|
rollbackId: deployment.rollback.rollbackId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(
|
||||||
|
"Rollback initiated successfully",
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error initiating rollback");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
isLoading={isRollingBack}
|
||||||
|
>
|
||||||
|
<RefreshCcw className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
Rollback
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ShowDeployment
|
<ShowDeployment
|
||||||
serverId={data?.serverId || ""}
|
serverId={serverId}
|
||||||
open={Boolean(activeLog && activeLog.logPath !== null)}
|
open={Boolean(activeLog && activeLog.logPath !== null)}
|
||||||
onClose={() => setActiveLog(null)}
|
onClose={() => setActiveLog(null)}
|
||||||
logPath={activeLog?.logPath || ""}
|
logPath={activeLog?.logPath || ""}
|
||||||
|
|||||||
@@ -1,391 +0,0 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input, NumberInput } 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 { 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 { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { Dices } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import type z from "zod";
|
|
||||||
|
|
||||||
type Domain = z.infer<typeof domain>;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
applicationId: string;
|
|
||||||
domainId?: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AddDomain = ({
|
|
||||||
applicationId,
|
|
||||||
domainId = "",
|
|
||||||
children,
|
|
||||||
}: Props) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const { data, refetch } = api.domain.one.useQuery(
|
|
||||||
{
|
|
||||||
domainId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!domainId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: application } = api.application.one.useQuery(
|
|
||||||
{
|
|
||||||
applicationId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!applicationId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutateAsync, isError, error, isLoading } = domainId
|
|
||||||
? api.domain.update.useMutation()
|
|
||||||
: api.domain.create.useMutation();
|
|
||||||
|
|
||||||
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
|
|
||||||
api.domain.generateDomain.useMutation();
|
|
||||||
|
|
||||||
const { data: canGenerateTraefikMeDomains } =
|
|
||||||
api.domain.canGenerateTraefikMeDomains.useQuery({
|
|
||||||
serverId: application?.serverId || "",
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("canGenerateTraefikMeDomains", canGenerateTraefikMeDomains);
|
|
||||||
|
|
||||||
const form = useForm<Domain>({
|
|
||||||
resolver: zodResolver(domain),
|
|
||||||
defaultValues: {
|
|
||||||
host: "",
|
|
||||||
path: undefined,
|
|
||||||
port: undefined,
|
|
||||||
https: false,
|
|
||||||
certificateType: undefined,
|
|
||||||
customCertResolver: undefined,
|
|
||||||
},
|
|
||||||
mode: "onChange",
|
|
||||||
});
|
|
||||||
|
|
||||||
const certificateType = form.watch("certificateType");
|
|
||||||
const https = form.watch("https");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
form.reset({
|
|
||||||
...data,
|
|
||||||
/* Convert null to undefined */
|
|
||||||
path: data?.path || undefined,
|
|
||||||
port: data?.port || undefined,
|
|
||||||
certificateType: data?.certificateType || undefined,
|
|
||||||
customCertResolver: data?.customCertResolver || undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!domainId) {
|
|
||||||
form.reset({
|
|
||||||
host: "",
|
|
||||||
path: undefined,
|
|
||||||
port: undefined,
|
|
||||||
https: false,
|
|
||||||
certificateType: undefined,
|
|
||||||
customCertResolver: undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [form, data, isLoading, domainId]);
|
|
||||||
|
|
||||||
// Separate effect for handling custom cert resolver validation
|
|
||||||
useEffect(() => {
|
|
||||||
if (certificateType === "custom") {
|
|
||||||
form.trigger("customCertResolver");
|
|
||||||
}
|
|
||||||
}, [certificateType, form]);
|
|
||||||
|
|
||||||
const dictionary = {
|
|
||||||
success: domainId ? "Domain Updated" : "Domain Created",
|
|
||||||
error: domainId ? "Error updating the domain" : "Error creating the domain",
|
|
||||||
submit: domainId ? "Update" : "Create",
|
|
||||||
dialogDescription: domainId
|
|
||||||
? "In this section you can edit a domain"
|
|
||||||
: "In this section you can add domains",
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async (data: Domain) => {
|
|
||||||
await mutateAsync({
|
|
||||||
domainId,
|
|
||||||
applicationId,
|
|
||||||
...data,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success(dictionary.success);
|
|
||||||
await utils.domain.byApplicationId.invalidate({
|
|
||||||
applicationId,
|
|
||||||
});
|
|
||||||
await utils.application.readTraefikConfig.invalidate({ applicationId });
|
|
||||||
|
|
||||||
if (domainId) {
|
|
||||||
refetch();
|
|
||||||
}
|
|
||||||
setIsOpen(false);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error(dictionary.error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger className="" asChild>
|
|
||||||
{children}
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Domain</DialogTitle>
|
|
||||||
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
id="hook-form"
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="grid w-full gap-8 "
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="host"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
{!canGenerateTraefikMeDomains &&
|
|
||||||
field.value.includes("traefik.me") && (
|
|
||||||
<AlertBlock type="warning">
|
|
||||||
You need to set an IP address in your{" "}
|
|
||||||
<Link
|
|
||||||
href="/dashboard/settings/server"
|
|
||||||
className="text-primary"
|
|
||||||
>
|
|
||||||
{application?.serverId
|
|
||||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
|
||||||
: "Web Server -> Server -> Update Server IP"}
|
|
||||||
</Link>{" "}
|
|
||||||
to make your traefik.me domain work.
|
|
||||||
</AlertBlock>
|
|
||||||
)}
|
|
||||||
<FormLabel>Host</FormLabel>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="api.dokploy.com" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
type="button"
|
|
||||||
isLoading={isLoadingGenerate}
|
|
||||||
onClick={() => {
|
|
||||||
generateDomain({
|
|
||||||
appName: application?.appName || "",
|
|
||||||
serverId: application?.serverId || "",
|
|
||||||
})
|
|
||||||
.then((domain) => {
|
|
||||||
field.onChange(domain);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error(err.message);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Dices className="size-4 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent
|
|
||||||
side="left"
|
|
||||||
sideOffset={5}
|
|
||||||
className="max-w-[10rem]"
|
|
||||||
>
|
|
||||||
<p>Generate traefik.me domain</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="path"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Path</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder={"/"} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="port"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Container Port</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<NumberInput placeholder={"3000"} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="https"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>HTTPS</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Automatically provision SSL Certificate.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{https && (
|
|
||||||
<>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="certificateType"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Certificate Provider</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={(value) => {
|
|
||||||
field.onChange(value);
|
|
||||||
if (value !== "custom") {
|
|
||||||
form.setValue(
|
|
||||||
"customCertResolver",
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
value={field.value}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a certificate provider" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value={"none"}>None</SelectItem>
|
|
||||||
<SelectItem value={"letsencrypt"}>
|
|
||||||
Let's Encrypt
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value={"custom"}>Custom</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{certificateType === "custom" && (
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="customCertResolver"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Custom Certificate Resolver</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
className="w-full"
|
|
||||||
placeholder="Enter your custom certificate resolver"
|
|
||||||
{...field}
|
|
||||||
value={field.value || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
field.onChange(e);
|
|
||||||
form.trigger("customCertResolver");
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button isLoading={isLoading} form="hook-form" type="submit">
|
|
||||||
{dictionary.submit}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { Copy, HelpCircle, Server } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
domain: {
|
||||||
|
host: string;
|
||||||
|
https: boolean;
|
||||||
|
path?: string;
|
||||||
|
};
|
||||||
|
serverIp?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DnsHelperModal = ({ domain, serverIp }: Props) => {
|
||||||
|
const copyToClipboard = (text: string) => {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
toast.success("Copied to clipboard!");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger>
|
||||||
|
<Button variant="ghost" size="icon" className="group">
|
||||||
|
<HelpCircle className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Server className="size-5" />
|
||||||
|
DNS Configuration Guide
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Follow these steps to configure your DNS records for {domain.host}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<AlertBlock type="info">
|
||||||
|
To make your domain accessible, you need to configure your DNS
|
||||||
|
records with your domain provider (e.g., Cloudflare, GoDaddy,
|
||||||
|
NameCheap).
|
||||||
|
</AlertBlock>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="rounded-lg border p-4">
|
||||||
|
<h3 className="font-medium mb-2">1. Add A Record</h3>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Create an A record that points your domain to the server's IP
|
||||||
|
address:
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center justify-between gap-2 bg-muted p-3 rounded-md">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">Type: A</p>
|
||||||
|
<p className="text-sm">
|
||||||
|
Name: @ or {domain.host.split(".")[0]}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm">
|
||||||
|
Value: {serverIp || "Your server IP"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => copyToClipboard(serverIp || "")}
|
||||||
|
disabled={!serverIp}
|
||||||
|
>
|
||||||
|
<Copy className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg border p-4">
|
||||||
|
<h3 className="font-medium mb-2">2. Verify Configuration</h3>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
After configuring your DNS records:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc list-inside space-y-1 text-sm">
|
||||||
|
<li>Wait for DNS propagation (usually 15-30 minutes)</li>
|
||||||
|
<li>
|
||||||
|
Test your domain by visiting:{" "}
|
||||||
|
{domain.https ? "https://" : "http://"}
|
||||||
|
{domain.host}
|
||||||
|
{domain.path || "/"}
|
||||||
|
</li>
|
||||||
|
<li>Use a DNS lookup tool to verify your records</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,719 @@
|
|||||||
|
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 { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input, NumberInput } 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 { api } from "@/utils/api";
|
||||||
|
|
||||||
|
export type CacheType = "fetch" | "cache";
|
||||||
|
|
||||||
|
export const domain = z
|
||||||
|
.object({
|
||||||
|
host: z.string().min(1, { message: "Add a hostname" }),
|
||||||
|
path: z.string().min(1).optional(),
|
||||||
|
internalPath: z.string().optional(),
|
||||||
|
stripPath: z.boolean().optional(),
|
||||||
|
port: z
|
||||||
|
.number()
|
||||||
|
.min(1, { message: "Port must be at least 1" })
|
||||||
|
.max(65535, { message: "Port must be 65535 or below" })
|
||||||
|
.optional(),
|
||||||
|
https: z.boolean().optional(),
|
||||||
|
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
||||||
|
customCertResolver: z.string().optional(),
|
||||||
|
serviceName: z.string().optional(),
|
||||||
|
domainType: z.enum(["application", "compose", "preview"]).optional(),
|
||||||
|
})
|
||||||
|
.superRefine((input, ctx) => {
|
||||||
|
if (input.https && !input.certificateType) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["certificateType"],
|
||||||
|
message: "Required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.certificateType === "custom" && !input.customCertResolver) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["customCertResolver"],
|
||||||
|
message: "Required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.domainType === "compose" && !input.serviceName) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["serviceName"],
|
||||||
|
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>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
type: "application" | "compose";
|
||||||
|
domainId?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
||||||
|
const [isManualInput, setIsManualInput] = useState(false);
|
||||||
|
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { data, refetch } = api.domain.one.useQuery(
|
||||||
|
{
|
||||||
|
domainId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!domainId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: application } =
|
||||||
|
type === "application"
|
||||||
|
? api.application.one.useQuery(
|
||||||
|
{
|
||||||
|
applicationId: id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: api.compose.one.useQuery(
|
||||||
|
{
|
||||||
|
composeId: id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync, isError, error, isLoading } = domainId
|
||||||
|
? api.domain.update.useMutation()
|
||||||
|
: api.domain.create.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
|
||||||
|
api.domain.generateDomain.useMutation();
|
||||||
|
|
||||||
|
const { data: canGenerateTraefikMeDomains } =
|
||||||
|
api.domain.canGenerateTraefikMeDomains.useQuery({
|
||||||
|
serverId: application?.serverId || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: services,
|
||||||
|
isFetching: isLoadingServices,
|
||||||
|
error: errorServices,
|
||||||
|
refetch: refetchServices,
|
||||||
|
} = api.compose.loadServices.useQuery(
|
||||||
|
{
|
||||||
|
composeId: id,
|
||||||
|
type: cacheType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
retry: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
enabled: type === "compose" && !!id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const form = useForm<Domain>({
|
||||||
|
resolver: zodResolver(domain),
|
||||||
|
defaultValues: {
|
||||||
|
host: "",
|
||||||
|
path: undefined,
|
||||||
|
internalPath: undefined,
|
||||||
|
stripPath: false,
|
||||||
|
port: undefined,
|
||||||
|
https: false,
|
||||||
|
certificateType: undefined,
|
||||||
|
customCertResolver: undefined,
|
||||||
|
serviceName: undefined,
|
||||||
|
domainType: type,
|
||||||
|
},
|
||||||
|
mode: "onChange",
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificateType = form.watch("certificateType");
|
||||||
|
const https = form.watch("https");
|
||||||
|
const domainType = form.watch("domainType");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
...data,
|
||||||
|
/* Convert null to undefined */
|
||||||
|
path: data?.path || undefined,
|
||||||
|
internalPath: data?.internalPath || undefined,
|
||||||
|
stripPath: data?.stripPath || false,
|
||||||
|
port: data?.port || undefined,
|
||||||
|
certificateType: data?.certificateType || undefined,
|
||||||
|
customCertResolver: data?.customCertResolver || undefined,
|
||||||
|
serviceName: data?.serviceName || undefined,
|
||||||
|
domainType: data?.domainType || type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!domainId) {
|
||||||
|
form.reset({
|
||||||
|
host: "",
|
||||||
|
path: undefined,
|
||||||
|
internalPath: undefined,
|
||||||
|
stripPath: false,
|
||||||
|
port: undefined,
|
||||||
|
https: false,
|
||||||
|
certificateType: undefined,
|
||||||
|
customCertResolver: undefined,
|
||||||
|
domainType: type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form, data, isLoading, domainId]);
|
||||||
|
|
||||||
|
// Separate effect for handling custom cert resolver validation
|
||||||
|
useEffect(() => {
|
||||||
|
if (certificateType === "custom") {
|
||||||
|
form.trigger("customCertResolver");
|
||||||
|
}
|
||||||
|
}, [certificateType, form]);
|
||||||
|
|
||||||
|
const dictionary = {
|
||||||
|
success: domainId ? "Domain Updated" : "Domain Created",
|
||||||
|
error: domainId ? "Error updating the domain" : "Error creating the domain",
|
||||||
|
submit: domainId ? "Update" : "Create",
|
||||||
|
dialogDescription: domainId
|
||||||
|
? "In this section you can edit a domain"
|
||||||
|
: "In this section you can add domains",
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data: Domain) => {
|
||||||
|
await mutateAsync({
|
||||||
|
domainId,
|
||||||
|
...(data.domainType === "application" && {
|
||||||
|
applicationId: id,
|
||||||
|
}),
|
||||||
|
...(data.domainType === "compose" && {
|
||||||
|
composeId: id,
|
||||||
|
}),
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success(dictionary.success);
|
||||||
|
|
||||||
|
if (data.domainType === "application") {
|
||||||
|
await utils.domain.byApplicationId.invalidate({
|
||||||
|
applicationId: id,
|
||||||
|
});
|
||||||
|
await utils.application.readTraefikConfig.invalidate({
|
||||||
|
applicationId: id,
|
||||||
|
});
|
||||||
|
} else if (data.domainType === "compose") {
|
||||||
|
await utils.domain.byComposeId.invalidate({
|
||||||
|
composeId: id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domainId) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
toast.error(dictionary.error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger className="" asChild>
|
||||||
|
{children}
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Domain</DialogTitle>
|
||||||
|
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-8 "
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex flex-row items-end w-full gap-4">
|
||||||
|
{domainType === "compose" && (
|
||||||
|
<div className="flex flex-col gap-2 w-full">
|
||||||
|
{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">
|
||||||
|
{isManualInput ? (
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter service name manually"
|
||||||
|
{...field}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
) : (
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
{!isManualInput && (
|
||||||
|
<>
|
||||||
|
<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}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setIsManualInput(!isManualInput);
|
||||||
|
if (!isManualInput) {
|
||||||
|
field.onChange("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isManualInput ? (
|
||||||
|
<RefreshCw className="size-4 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Manual
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{isManualInput
|
||||||
|
? "Switch to service selection"
|
||||||
|
: "Enter service name manually"}
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="host"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
{!canGenerateTraefikMeDomains &&
|
||||||
|
field.value.includes("traefik.me") && (
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
You need to set an IP address in your{" "}
|
||||||
|
<Link
|
||||||
|
href="/dashboard/settings/server"
|
||||||
|
className="text-primary"
|
||||||
|
>
|
||||||
|
{application?.serverId
|
||||||
|
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||||
|
: "Web Server -> Server -> Update Server IP"}
|
||||||
|
</Link>{" "}
|
||||||
|
to make your traefik.me domain work.
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
<FormLabel>Host</FormLabel>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="api.dokploy.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingGenerate}
|
||||||
|
onClick={() => {
|
||||||
|
generateDomain({
|
||||||
|
appName: application?.appName || "",
|
||||||
|
serverId: application?.serverId || "",
|
||||||
|
})
|
||||||
|
.then((domain) => {
|
||||||
|
field.onChange(domain);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(err.message);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dices className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>Generate traefik.me domain</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="path"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Path</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder={"/"} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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
|
||||||
|
control={form.control}
|
||||||
|
name="port"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Container Port</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
The port where your application is running inside the
|
||||||
|
container (e.g., 3000 for Node.js, 80 for Nginx, 8080
|
||||||
|
for Java)
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<NumberInput placeholder={"3000"} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="https"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>HTTPS</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Automatically provision SSL Certificate.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{https && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="certificateType"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Certificate Provider</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
if (value !== "custom") {
|
||||||
|
form.setValue(
|
||||||
|
"customCertResolver",
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
value={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a certificate provider" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={"none"}>None</SelectItem>
|
||||||
|
<SelectItem value={"letsencrypt"}>
|
||||||
|
Let's Encrypt
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value={"custom"}>Custom</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{certificateType === "custom" && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="customCertResolver"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Custom Certificate Resolver</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Enter your custom certificate resolver"
|
||||||
|
{...field}
|
||||||
|
value={field.value || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
field.onChange(e);
|
||||||
|
form.trigger("customCertResolver");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button isLoading={isLoading} form="hook-form" type="submit">
|
||||||
|
{dictionary.submit}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,20 @@
|
|||||||
|
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 { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -7,29 +23,120 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { ExternalLink, GlobeIcon, PenBoxIcon, Trash2 } from "lucide-react";
|
import { DnsHelperModal } from "./dns-helper-modal";
|
||||||
import Link from "next/link";
|
import { AddDomain } from "./handle-domain";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { AddDomain } from "./add-domain";
|
export type ValidationState = {
|
||||||
|
isLoading: boolean;
|
||||||
|
isValid?: boolean;
|
||||||
|
error?: string;
|
||||||
|
resolvedIp?: string;
|
||||||
|
message?: string;
|
||||||
|
cdnProvider?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ValidationStates = Record<string, ValidationState>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
id: string;
|
||||||
|
type: "application" | "compose";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDomains = ({ applicationId }: Props) => {
|
export const ShowDomains = ({ id, type }: Props) => {
|
||||||
const { data, refetch } = api.domain.byApplicationId.useQuery(
|
const { data: application } =
|
||||||
{
|
type === "application"
|
||||||
applicationId,
|
? api.application.one.useQuery(
|
||||||
},
|
{
|
||||||
{
|
applicationId: id,
|
||||||
enabled: !!applicationId,
|
},
|
||||||
},
|
{
|
||||||
|
enabled: !!id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: api.compose.one.useQuery(
|
||||||
|
{
|
||||||
|
composeId: id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const [validationStates, setValidationStates] = useState<ValidationStates>(
|
||||||
|
{},
|
||||||
);
|
);
|
||||||
|
const { data: ip } = api.settings.getIp.useQuery();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
refetch,
|
||||||
|
isLoading: isLoadingDomains,
|
||||||
|
} = type === "application"
|
||||||
|
? api.domain.byApplicationId.useQuery(
|
||||||
|
{
|
||||||
|
applicationId: id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: api.domain.byComposeId.useQuery(
|
||||||
|
{
|
||||||
|
composeId: id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync: validateDomain } =
|
||||||
|
api.domain.validateDomain.useMutation();
|
||||||
const { mutateAsync: deleteDomain, isLoading: isRemoving } =
|
const { mutateAsync: deleteDomain, isLoading: isRemoving } =
|
||||||
api.domain.delete.useMutation();
|
api.domain.delete.useMutation();
|
||||||
|
|
||||||
|
const handleValidateDomain = async (host: string) => {
|
||||||
|
setValidationStates((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[host]: { isLoading: true },
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await validateDomain({
|
||||||
|
domain: host,
|
||||||
|
serverIp:
|
||||||
|
application?.server?.ipAddress?.toString() || ip?.toString() || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
setValidationStates((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[host]: {
|
||||||
|
isLoading: false,
|
||||||
|
isValid: result.isValid,
|
||||||
|
error: result.error,
|
||||||
|
resolvedIp: result.resolvedIp,
|
||||||
|
cdnProvider: result.cdnProvider,
|
||||||
|
message: result.error && result.isValid ? result.error : undefined,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
const error = err as Error;
|
||||||
|
setValidationStates((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[host]: {
|
||||||
|
isLoading: false,
|
||||||
|
isValid: false,
|
||||||
|
error: error.message || "Failed to validate domain",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
@@ -43,7 +150,7 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
|
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
{data && data?.length > 0 && (
|
{data && data?.length > 0 && (
|
||||||
<AddDomain applicationId={applicationId}>
|
<AddDomain id={id} type={type}>
|
||||||
<Button>
|
<Button>
|
||||||
<GlobeIcon className="size-4" /> Add Domain
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
</Button>
|
</Button>
|
||||||
@@ -52,15 +159,22 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-row gap-4">
|
<CardContent className="flex w-full flex-row gap-4">
|
||||||
{data?.length === 0 ? (
|
{isLoadingDomains ? (
|
||||||
<div className="flex w-full flex-col items-center justify-center gap-3">
|
<div className="flex w-full flex-row gap-4 min-h-[40vh] justify-center items-center">
|
||||||
|
<Loader2 className="size-5 animate-spin text-muted-foreground" />
|
||||||
|
<span className="text-base text-muted-foreground">
|
||||||
|
Loading domains...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : data?.length === 0 ? (
|
||||||
|
<div className="flex w-full flex-col items-center justify-center gap-3 min-h-[40vh]">
|
||||||
<GlobeIcon className="size-8 text-muted-foreground" />
|
<GlobeIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To access the application it is required to set at least 1
|
To access the application it is required to set at least 1
|
||||||
domain
|
domain
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
<AddDomain applicationId={applicationId}>
|
<AddDomain id={id} type={type}>
|
||||||
<Button>
|
<Button>
|
||||||
<GlobeIcon className="size-4" /> Add Domain
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
</Button>
|
</Button>
|
||||||
@@ -68,73 +182,216 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex w-full flex-col gap-4">
|
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2 w-full min-h-[40vh] ">
|
||||||
{data?.map((item) => {
|
{data?.map((item) => {
|
||||||
|
const validationState = validationStates[item.host];
|
||||||
return (
|
return (
|
||||||
<div
|
<Card
|
||||||
key={item.domainId}
|
key={item.domainId}
|
||||||
className="flex w-full items-center justify-between gap-4 border p-4 md:px-6 rounded-lg flex-wrap"
|
className="relative overflow-hidden w-full border transition-all hover:shadow-md bg-transparent h-fit"
|
||||||
>
|
>
|
||||||
<Link
|
<CardContent className="p-6">
|
||||||
className="md:basis-1/2 flex gap-2 items-center hover:underline transition-all w-full"
|
<div className="flex flex-col gap-4">
|
||||||
target="_blank"
|
{/* Service & Domain Info */}
|
||||||
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
|
<div className="flex items-center justify-between flex-wrap gap-y-2">
|
||||||
>
|
{item.serviceName && (
|
||||||
<span className="truncate max-w-full text-sm">
|
<Badge variant="outline" className="w-fit">
|
||||||
{item.host}
|
<Server className="size-3 mr-1" />
|
||||||
</span>
|
{item.serviceName}
|
||||||
<ExternalLink className="size-4 min-w-4" />
|
</Badge>
|
||||||
</Link>
|
)}
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
<div className="flex gap-8">
|
{!item.host.includes("traefik.me") && (
|
||||||
<div className="flex gap-8 opacity-50 items-center h-10 text-center text-sm font-medium">
|
<DnsHelperModal
|
||||||
<span>{item.path}</span>
|
domain={{
|
||||||
<span>{item.port}</span>
|
host: item.host,
|
||||||
<span>{item.https ? "HTTPS" : "HTTP"}</span>
|
https: item.https,
|
||||||
</div>
|
path: item.path || undefined,
|
||||||
|
}}
|
||||||
<div className="flex gap-2">
|
serverIp={
|
||||||
<AddDomain
|
application?.server?.ipAddress?.toString() ||
|
||||||
applicationId={applicationId}
|
ip?.toString()
|
||||||
domainId={item.domainId}
|
}
|
||||||
>
|
/>
|
||||||
<Button
|
)}
|
||||||
variant="ghost"
|
<AddDomain
|
||||||
size="icon"
|
id={id}
|
||||||
className="group hover:bg-blue-500/10 "
|
type={type}
|
||||||
|
domainId={item.domainId}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</AddDomain>
|
||||||
|
<DialogAction
|
||||||
|
title="Delete Domain"
|
||||||
|
description="Are you sure you want to delete this domain?"
|
||||||
|
type="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
await deleteDomain({
|
||||||
|
domainId: item.domainId,
|
||||||
|
})
|
||||||
|
.then((_data) => {
|
||||||
|
refetch();
|
||||||
|
toast.success(
|
||||||
|
"Domain deleted successfully",
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deleting domain");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10"
|
||||||
|
isLoading={isRemoving}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full break-all">
|
||||||
|
<Link
|
||||||
|
className="flex items-center gap-2 text-base font-medium hover:underline"
|
||||||
|
target="_blank"
|
||||||
|
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
|
||||||
>
|
>
|
||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
{item.host}
|
||||||
</Button>
|
<ExternalLink className="size-4 min-w-4" />
|
||||||
</AddDomain>
|
</Link>
|
||||||
<DialogAction
|
</div>
|
||||||
title="Delete Domain"
|
|
||||||
description="Are you sure you want to delete this domain?"
|
{/* Domain Details */}
|
||||||
type="destructive"
|
<div className="flex flex-wrap gap-3">
|
||||||
onClick={async () => {
|
<TooltipProvider>
|
||||||
await deleteDomain({
|
<Tooltip>
|
||||||
domainId: item.domainId,
|
<TooltipTrigger asChild>
|
||||||
})
|
<Badge variant="secondary">
|
||||||
.then(() => {
|
<InfoIcon className="size-3 mr-1" />
|
||||||
refetch();
|
Path: {item.path || "/"}
|
||||||
toast.success("Domain deleted successfully");
|
</Badge>
|
||||||
})
|
</TooltipTrigger>
|
||||||
.catch(() => {
|
<TooltipContent>
|
||||||
toast.error("Error deleting domain");
|
<p>URL path for this service</p>
|
||||||
});
|
</TooltipContent>
|
||||||
}}
|
</Tooltip>
|
||||||
>
|
</TooltipProvider>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
<TooltipProvider>
|
||||||
size="icon"
|
<Tooltip>
|
||||||
className="group hover:bg-red-500/10"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isRemoving}
|
<Badge variant="secondary">
|
||||||
>
|
<InfoIcon className="size-3 mr-1" />
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
Port: {item.port}
|
||||||
</Button>
|
</Badge>
|
||||||
</DialogAction>
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Container port exposed</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Badge
|
||||||
|
variant={item.https ? "outline" : "secondary"}
|
||||||
|
>
|
||||||
|
{item.https ? "HTTPS" : "HTTP"}
|
||||||
|
</Badge>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
{item.https
|
||||||
|
? "Secure HTTPS connection"
|
||||||
|
: "Standard HTTP connection"}
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
{item.certificateType && (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Badge variant="outline">
|
||||||
|
Cert: {item.certificateType}
|
||||||
|
</Badge>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>SSL Certificate Provider</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={
|
||||||
|
validationState?.isValid
|
||||||
|
? "bg-green-500/10 text-green-500 cursor-pointer"
|
||||||
|
: validationState?.error
|
||||||
|
? "bg-red-500/10 text-red-500 cursor-pointer"
|
||||||
|
: "bg-yellow-500/10 text-yellow-500 cursor-pointer"
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
handleValidateDomain(item.host)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{validationState?.isLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="size-3 mr-1 animate-spin" />
|
||||||
|
Checking DNS...
|
||||||
|
</>
|
||||||
|
) : validationState?.isValid ? (
|
||||||
|
<>
|
||||||
|
<CheckCircle2 className="size-3 mr-1" />
|
||||||
|
{validationState.message &&
|
||||||
|
validationState.cdnProvider
|
||||||
|
? `Behind ${validationState.cdnProvider}`
|
||||||
|
: "DNS Valid"}
|
||||||
|
</>
|
||||||
|
) : validationState?.error ? (
|
||||||
|
<>
|
||||||
|
<XCircle className="size-3 mr-1" />
|
||||||
|
{validationState.error}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="size-3 mr-1" />
|
||||||
|
Validate DNS
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-xs">
|
||||||
|
{validationState?.error ? (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<p className="font-medium text-red-500">
|
||||||
|
Error:
|
||||||
|
</p>
|
||||||
|
<p>{validationState.error}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
"Click to validate DNS configuration"
|
||||||
|
)}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -31,6 +38,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -39,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("/"),
|
||||||
@@ -58,6 +59,7 @@ const BitbucketProviderSchema = z.object({
|
|||||||
branch: z.string().min(1, "Branch is required"),
|
branch: z.string().min(1, "Branch is required"),
|
||||||
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
|
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
|
enableSubmodules: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
|
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
|
||||||
@@ -84,6 +86,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
bitbucketId: "",
|
bitbucketId: "",
|
||||||
branch: "",
|
branch: "",
|
||||||
watchPaths: [],
|
watchPaths: [],
|
||||||
|
enableSubmodules: false,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(BitbucketProviderSchema),
|
resolver: zodResolver(BitbucketProviderSchema),
|
||||||
});
|
});
|
||||||
@@ -130,9 +133,10 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
buildPath: data.bitbucketBuildPath || "/",
|
buildPath: data.bitbucketBuildPath || "/",
|
||||||
bitbucketId: data.bitbucketId || "",
|
bitbucketId: data.bitbucketId || "",
|
||||||
watchPaths: data.watchPaths || [],
|
watchPaths: data.watchPaths || [],
|
||||||
|
enableSubmodules: data.enableSubmodules || false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data?.applicationId, form]);
|
||||||
|
|
||||||
const onSubmit = async (data: BitbucketProvider) => {
|
const onSubmit = async (data: BitbucketProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -143,6 +147,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
bitbucketId: data.bitbucketId,
|
bitbucketId: data.bitbucketId,
|
||||||
applicationId,
|
applicationId,
|
||||||
watchPaths: data.watchPaths || [],
|
watchPaths: data.watchPaths || [],
|
||||||
|
enableSubmodules: data.enableSubmodules || false,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provided Saved");
|
||||||
@@ -430,7 +435,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -449,7 +454,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const input = document.querySelector(
|
const input = document.querySelector(
|
||||||
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
|
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
const value = input.value.trim();
|
const value = input.value.trim();
|
||||||
if (value) {
|
if (value) {
|
||||||
@@ -467,6 +472,21 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="enableSubmodules"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center space-x-2">
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -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, {
|
||||||
@@ -53,7 +53,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
|||||||
registryURL: data.registryUrl || "",
|
registryURL: data.registryUrl || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data?.applicationId, form]);
|
||||||
|
|
||||||
const onSubmit = async (values: DockerProvider) => {
|
const onSubmit = async (values: DockerProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -17,6 +27,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -24,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("/"),
|
||||||
@@ -44,6 +44,7 @@ const GitProviderSchema = z.object({
|
|||||||
branch: z.string().min(1, "Branch required"),
|
branch: z.string().min(1, "Branch required"),
|
||||||
sshKey: z.string().optional(),
|
sshKey: z.string().optional(),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
|
enableSubmodules: z.boolean().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
type GitProvider = z.infer<typeof GitProviderSchema>;
|
type GitProvider = z.infer<typeof GitProviderSchema>;
|
||||||
@@ -67,6 +68,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
repositoryURL: "",
|
repositoryURL: "",
|
||||||
sshKey: undefined,
|
sshKey: undefined,
|
||||||
watchPaths: [],
|
watchPaths: [],
|
||||||
|
enableSubmodules: false,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(GitProviderSchema),
|
resolver: zodResolver(GitProviderSchema),
|
||||||
});
|
});
|
||||||
@@ -79,6 +81,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
buildPath: data.customGitBuildPath || "/",
|
buildPath: data.customGitBuildPath || "/",
|
||||||
repositoryURL: data.customGitUrl || "",
|
repositoryURL: data.customGitUrl || "",
|
||||||
watchPaths: data.watchPaths || [],
|
watchPaths: data.watchPaths || [],
|
||||||
|
enableSubmodules: data.enableSubmodules ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
@@ -91,6 +94,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey,
|
customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey,
|
||||||
applicationId,
|
applicationId,
|
||||||
watchPaths: values.watchPaths || [],
|
watchPaths: values.watchPaths || [],
|
||||||
|
enableSubmodules: values.enableSubmodules,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Git Provider Saved");
|
toast.success("Git Provider Saved");
|
||||||
@@ -257,7 +261,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -276,7 +280,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const input = document.querySelector(
|
const input = document.querySelector(
|
||||||
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
|
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
const value = input.value.trim();
|
const value = input.value.trim();
|
||||||
if (value) {
|
if (value) {
|
||||||
@@ -294,6 +298,22 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="enableSubmodules"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center space-x-2">
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row justify-end">
|
<div className="flex flex-row justify-end">
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -31,6 +38,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -39,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;
|
||||||
@@ -74,6 +75,7 @@ const GiteaProviderSchema = z.object({
|
|||||||
branch: z.string().min(1, "Branch is required"),
|
branch: z.string().min(1, "Branch is required"),
|
||||||
giteaId: z.string().min(1, "Gitea Provider is required"),
|
giteaId: z.string().min(1, "Gitea Provider is required"),
|
||||||
watchPaths: z.array(z.string()).default([]),
|
watchPaths: z.array(z.string()).default([]),
|
||||||
|
enableSubmodules: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
|
type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
|
||||||
@@ -99,6 +101,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
|||||||
giteaId: "",
|
giteaId: "",
|
||||||
branch: "",
|
branch: "",
|
||||||
watchPaths: [],
|
watchPaths: [],
|
||||||
|
enableSubmodules: false,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(GiteaProviderSchema),
|
resolver: zodResolver(GiteaProviderSchema),
|
||||||
});
|
});
|
||||||
@@ -152,9 +155,10 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
|||||||
buildPath: data.giteaBuildPath || "/",
|
buildPath: data.giteaBuildPath || "/",
|
||||||
giteaId: data.giteaId || "",
|
giteaId: data.giteaId || "",
|
||||||
watchPaths: data.watchPaths || [],
|
watchPaths: data.watchPaths || [],
|
||||||
|
enableSubmodules: data.enableSubmodules || false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data?.applicationId, form]);
|
||||||
|
|
||||||
const onSubmit = async (data: GiteaProvider) => {
|
const onSubmit = async (data: GiteaProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -165,6 +169,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
|||||||
giteaId: data.giteaId,
|
giteaId: data.giteaId,
|
||||||
applicationId,
|
applicationId,
|
||||||
watchPaths: data.watchPaths,
|
watchPaths: data.watchPaths,
|
||||||
|
enableSubmodules: data.enableSubmodules || false,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provider Saved");
|
toast.success("Service Provider Saved");
|
||||||
@@ -376,6 +381,9 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
|||||||
<CommandEmpty>No branch found.</CommandEmpty>
|
<CommandEmpty>No branch found.</CommandEmpty>
|
||||||
|
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
|
{branches && branches.length === 0 && (
|
||||||
|
<CommandItem>No branches found.</CommandItem>
|
||||||
|
)}
|
||||||
{branches?.map((branch: GiteaBranch) => (
|
{branches?.map((branch: GiteaBranch) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={branch.name}
|
value={branch.name}
|
||||||
@@ -462,7 +470,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -498,6 +506,21 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="enableSubmodules"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center space-x-2">
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -30,6 +37,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -38,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("/"),
|
||||||
@@ -57,6 +58,8 @@ const GithubProviderSchema = z.object({
|
|||||||
branch: z.string().min(1, "Branch is required"),
|
branch: z.string().min(1, "Branch is required"),
|
||||||
githubId: z.string().min(1, "Github Provider is required"),
|
githubId: z.string().min(1, "Github Provider is required"),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
|
triggerType: z.enum(["push", "tag"]).default("push"),
|
||||||
|
enableSubmodules: z.boolean().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
type GithubProvider = z.infer<typeof GithubProviderSchema>;
|
type GithubProvider = z.infer<typeof GithubProviderSchema>;
|
||||||
@@ -81,12 +84,15 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
},
|
},
|
||||||
githubId: "",
|
githubId: "",
|
||||||
branch: "",
|
branch: "",
|
||||||
|
triggerType: "push",
|
||||||
|
enableSubmodules: false,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(GithubProviderSchema),
|
resolver: zodResolver(GithubProviderSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const repository = form.watch("repository");
|
const repository = form.watch("repository");
|
||||||
const githubId = form.watch("githubId");
|
const githubId = form.watch("githubId");
|
||||||
|
const triggerType = form.watch("triggerType");
|
||||||
|
|
||||||
const { data: repositories, isLoading: isLoadingRepositories } =
|
const { data: repositories, isLoading: isLoadingRepositories } =
|
||||||
api.github.getGithubRepositories.useQuery(
|
api.github.getGithubRepositories.useQuery(
|
||||||
@@ -124,9 +130,11 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
buildPath: data.buildPath || "/",
|
buildPath: data.buildPath || "/",
|
||||||
githubId: data.githubId || "",
|
githubId: data.githubId || "",
|
||||||
watchPaths: data.watchPaths || [],
|
watchPaths: data.watchPaths || [],
|
||||||
|
triggerType: data.triggerType || "push",
|
||||||
|
enableSubmodules: data.enableSubmodules ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data?.applicationId, form]);
|
||||||
|
|
||||||
const onSubmit = async (data: GithubProvider) => {
|
const onSubmit = async (data: GithubProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -137,6 +145,8 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
buildPath: data.buildPath,
|
buildPath: data.buildPath,
|
||||||
githubId: data.githubId,
|
githubId: data.githubId,
|
||||||
watchPaths: data.watchPaths || [],
|
watchPaths: data.watchPaths || [],
|
||||||
|
triggerType: data.triggerType,
|
||||||
|
enableSubmodules: data.enableSubmodules,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provided Saved");
|
||||||
@@ -381,11 +391,11 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="watchPaths"
|
name="triggerType"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="md:col-span-2">
|
<FormItem className="md:col-span-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 ">
|
||||||
<FormLabel>Watch Paths</FormLabel>
|
<FormLabel>Trigger Type</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -393,68 +403,127 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>
|
<p>
|
||||||
Add paths to watch for changes. When files in these
|
Choose when to trigger deployments: on push to the
|
||||||
paths change, a new deployment will be triggered.
|
selected branch or when a new tag is created.
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2 mb-2">
|
<Select
|
||||||
{field.value?.map((path, index) => (
|
onValueChange={field.onChange}
|
||||||
<Badge
|
defaultValue={field.value}
|
||||||
key={index}
|
value={field.value}
|
||||||
variant="secondary"
|
>
|
||||||
className="flex items-center gap-1"
|
<FormControl>
|
||||||
>
|
<SelectTrigger>
|
||||||
{path}
|
<SelectValue placeholder="Select a trigger type" />
|
||||||
<X
|
</SelectTrigger>
|
||||||
className="size-3 cursor-pointer hover:text-destructive"
|
</FormControl>
|
||||||
onClick={() => {
|
<SelectContent>
|
||||||
const newPaths = [...(field.value || [])];
|
<SelectItem value="push">On Push</SelectItem>
|
||||||
newPaths.splice(index, 1);
|
<SelectItem value="tag">On Tag</SelectItem>
|
||||||
field.onChange(newPaths);
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{triggerType === "push" && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="watchPaths"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Add paths to watch for changes. When files in
|
||||||
|
these paths change, a new deployment will be
|
||||||
|
triggered.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-2">
|
||||||
|
{field.value?.map((path, index) => (
|
||||||
|
<Badge
|
||||||
|
key={index}
|
||||||
|
variant="secondary"
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{path}
|
||||||
|
<X
|
||||||
|
className="size-3 cursor-pointer hover:text-destructive"
|
||||||
|
onClick={() => {
|
||||||
|
const newPaths = [...(field.value || [])];
|
||||||
|
newPaths.splice(index, 1);
|
||||||
|
field.onChange(newPaths);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
const input = e.currentTarget;
|
||||||
|
const path = input.value.trim();
|
||||||
|
if (path) {
|
||||||
|
field.onChange([...(field.value || []), path]);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Badge>
|
</FormControl>
|
||||||
))}
|
<Button
|
||||||
</div>
|
type="button"
|
||||||
<div className="flex gap-2">
|
variant="outline"
|
||||||
<FormControl>
|
size="icon"
|
||||||
<Input
|
onClick={() => {
|
||||||
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
const input = document.querySelector(
|
||||||
onKeyDown={(e) => {
|
'input[placeholder*="Enter a path"]',
|
||||||
if (e.key === "Enter") {
|
) as HTMLInputElement;
|
||||||
e.preventDefault();
|
const path = input.value.trim();
|
||||||
const input = e.currentTarget;
|
if (path) {
|
||||||
const path = input.value.trim();
|
field.onChange([...(field.value || []), path]);
|
||||||
if (path) {
|
input.value = "";
|
||||||
field.onChange([...(field.value || []), path]);
|
|
||||||
input.value = "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
</FormControl>
|
<Plus className="size-4" />
|
||||||
<Button
|
</Button>
|
||||||
type="button"
|
</div>
|
||||||
variant="outline"
|
<FormMessage />
|
||||||
size="icon"
|
</FormItem>
|
||||||
onClick={() => {
|
)}
|
||||||
const input = document.querySelector(
|
/>
|
||||||
'input[placeholder*="Enter a path"]',
|
)}
|
||||||
) as HTMLInputElement;
|
|
||||||
const path = input.value.trim();
|
<FormField
|
||||||
if (path) {
|
control={form.control}
|
||||||
field.onChange([...(field.value || []), path]);
|
name="enableSubmodules"
|
||||||
input.value = "";
|
render={({ field }) => (
|
||||||
}
|
<FormItem className="flex items-center space-x-2">
|
||||||
}}
|
<FormControl>
|
||||||
>
|
<Switch
|
||||||
<Plus className="size-4" />
|
checked={field.value}
|
||||||
</Button>
|
onCheckedChange={field.onChange}
|
||||||
</div>
|
/>
|
||||||
<FormMessage />
|
</FormControl>
|
||||||
|
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -31,6 +38,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -39,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("/"),
|
||||||
@@ -60,6 +61,7 @@ const GitlabProviderSchema = z.object({
|
|||||||
branch: z.string().min(1, "Branch is required"),
|
branch: z.string().min(1, "Branch is required"),
|
||||||
gitlabId: z.string().min(1, "Gitlab Provider is required"),
|
gitlabId: z.string().min(1, "Gitlab Provider is required"),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
|
enableSubmodules: z.boolean().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
|
type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
|
||||||
@@ -86,6 +88,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
},
|
},
|
||||||
gitlabId: "",
|
gitlabId: "",
|
||||||
branch: "",
|
branch: "",
|
||||||
|
enableSubmodules: false,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(GitlabProviderSchema),
|
resolver: zodResolver(GitlabProviderSchema),
|
||||||
});
|
});
|
||||||
@@ -93,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,
|
||||||
@@ -135,9 +148,10 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
buildPath: data.gitlabBuildPath || "/",
|
buildPath: data.gitlabBuildPath || "/",
|
||||||
gitlabId: data.gitlabId || "",
|
gitlabId: data.gitlabId || "",
|
||||||
watchPaths: data.watchPaths || [],
|
watchPaths: data.watchPaths || [],
|
||||||
|
enableSubmodules: data.enableSubmodules ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data?.applicationId, form]);
|
||||||
|
|
||||||
const onSubmit = async (data: GitlabProvider) => {
|
const onSubmit = async (data: GitlabProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -150,6 +164,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
gitlabProjectId: data.repository.id,
|
gitlabProjectId: data.repository.id,
|
||||||
gitlabPathNamespace: data.repository.gitlabPathNamespace,
|
gitlabPathNamespace: data.repository.gitlabPathNamespace,
|
||||||
watchPaths: data.watchPaths || [],
|
watchPaths: data.watchPaths || [],
|
||||||
|
enableSubmodules: data.enableSubmodules,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provided Saved");
|
||||||
@@ -219,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"
|
||||||
@@ -273,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", {
|
||||||
@@ -294,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",
|
||||||
)}
|
)}
|
||||||
@@ -447,7 +463,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -483,6 +499,21 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="enableSubmodules"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center space-x-2">
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -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,20 +9,18 @@ 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, UploadCloud } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useState } from "react";
|
|
||||||
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";
|
||||||
|
import { UnauthorizedGitProvider } from "./unauthorized-git-provider";
|
||||||
|
|
||||||
type TabState =
|
type TabState =
|
||||||
| "github"
|
| "github"
|
||||||
@@ -34,14 +36,100 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowProviderForm = ({ applicationId }: Props) => {
|
export const ShowProviderForm = ({ applicationId }: Props) => {
|
||||||
const { data: githubProviders } = api.github.githubProviders.useQuery();
|
const { data: githubProviders, isLoading: isLoadingGithub } =
|
||||||
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
api.github.githubProviders.useQuery();
|
||||||
const { data: bitbucketProviders } =
|
const { data: gitlabProviders, isLoading: isLoadingGitlab } =
|
||||||
|
api.gitlab.gitlabProviders.useQuery();
|
||||||
|
const { data: bitbucketProviders, isLoading: isLoadingBitbucket } =
|
||||||
api.bitbucket.bitbucketProviders.useQuery();
|
api.bitbucket.bitbucketProviders.useQuery();
|
||||||
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
const { data: giteaProviders, isLoading: isLoadingGitea } =
|
||||||
|
api.gitea.giteaProviders.useQuery();
|
||||||
|
|
||||||
|
const { data: application, refetch } = api.application.one.useQuery({
|
||||||
|
applicationId,
|
||||||
|
});
|
||||||
|
const { mutateAsync: disconnectGitProvider } =
|
||||||
|
api.application.disconnectGitProvider.useMutation();
|
||||||
|
|
||||||
const { data: application } = api.application.one.useQuery({ applicationId });
|
|
||||||
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
|
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
|
||||||
|
|
||||||
|
const isLoading =
|
||||||
|
isLoadingGithub || isLoadingGitlab || isLoadingBitbucket || isLoadingGitea;
|
||||||
|
|
||||||
|
const handleDisconnect = async () => {
|
||||||
|
try {
|
||||||
|
await disconnectGitProvider({ applicationId });
|
||||||
|
toast.success("Repository disconnected successfully");
|
||||||
|
await refetch();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
`Failed to disconnect repository: ${
|
||||||
|
error instanceof Error ? error.message : "Unknown error"
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Card className="group relative w-full bg-transparent">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-start justify-between">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<span className="flex flex-col space-y-0.5">Provider</span>
|
||||||
|
<p className="flex items-center text-sm font-normal text-muted-foreground">
|
||||||
|
Select the source of your code
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="hidden space-y-1 text-sm font-normal md:block">
|
||||||
|
<GitBranch className="size-6 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex min-h-[25vh] items-center justify-center">
|
||||||
|
<div className="flex items-center gap-2 text-muted-foreground">
|
||||||
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
<span>Loading providers...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user doesn't have access to the current git provider
|
||||||
|
if (
|
||||||
|
application &&
|
||||||
|
!application.hasGitProviderAccess &&
|
||||||
|
application.sourceType !== "docker" &&
|
||||||
|
application.sourceType !== "drop"
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<Card className="group relative w-full bg-transparent">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-start justify-between">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<span className="flex flex-col space-y-0.5">Provider</span>
|
||||||
|
<p className="flex items-center text-sm font-normal text-muted-foreground">
|
||||||
|
Repository connection through unauthorized provider
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="hidden space-y-1 text-sm font-normal md:block">
|
||||||
|
<GitBranch className="size-6 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<UnauthorizedGitProvider
|
||||||
|
service={application}
|
||||||
|
onDisconnect={handleDisconnect}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="group relative w-full bg-transparent">
|
<Card className="group relative w-full bg-transparent">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -65,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"
|
||||||
@@ -123,7 +211,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
{githubProviders && githubProviders?.length > 0 ? (
|
{githubProviders && githubProviders?.length > 0 ? (
|
||||||
<SaveGithubProvider applicationId={applicationId} />
|
<SaveGithubProvider applicationId={applicationId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||||
<GithubIcon className="size-8 text-muted-foreground" />
|
<GithubIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To deploy using GitHub, you need to configure your account
|
To deploy using GitHub, you need to configure your account
|
||||||
@@ -143,7 +231,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
{gitlabProviders && gitlabProviders?.length > 0 ? (
|
{gitlabProviders && gitlabProviders?.length > 0 ? (
|
||||||
<SaveGitlabProvider applicationId={applicationId} />
|
<SaveGitlabProvider applicationId={applicationId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||||
<GitlabIcon className="size-8 text-muted-foreground" />
|
<GitlabIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To deploy using GitLab, you need to configure your account
|
To deploy using GitLab, you need to configure your account
|
||||||
@@ -163,7 +251,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
{bitbucketProviders && bitbucketProviders?.length > 0 ? (
|
{bitbucketProviders && bitbucketProviders?.length > 0 ? (
|
||||||
<SaveBitbucketProvider applicationId={applicationId} />
|
<SaveBitbucketProvider applicationId={applicationId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||||
<BitbucketIcon className="size-8 text-muted-foreground" />
|
<BitbucketIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To deploy using Bitbucket, you need to configure your account
|
To deploy using Bitbucket, you need to configure your account
|
||||||
@@ -183,7 +271,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
{giteaProviders && giteaProviders?.length > 0 ? (
|
{giteaProviders && giteaProviders?.length > 0 ? (
|
||||||
<SaveGiteaProvider applicationId={applicationId} />
|
<SaveGiteaProvider applicationId={applicationId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||||
<GiteaIcon className="size-8 text-muted-foreground" />
|
<GiteaIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To deploy using Gitea, you need to configure your account
|
To deploy using Gitea, you need to configure your account
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import { AlertCircle, GitBranch, Unlink } from "lucide-react";
|
||||||
|
import {
|
||||||
|
BitbucketIcon,
|
||||||
|
GiteaIcon,
|
||||||
|
GithubIcon,
|
||||||
|
GitIcon,
|
||||||
|
GitlabIcon,
|
||||||
|
} from "@/components/icons/data-tools-icons";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import type { RouterOutputs } from "@/utils/api";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
service:
|
||||||
|
| RouterOutputs["application"]["one"]
|
||||||
|
| RouterOutputs["compose"]["one"];
|
||||||
|
onDisconnect: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UnauthorizedGitProvider = ({ service, onDisconnect }: Props) => {
|
||||||
|
const getProviderIcon = (sourceType: string) => {
|
||||||
|
switch (sourceType) {
|
||||||
|
case "github":
|
||||||
|
return <GithubIcon className="size-5 text-muted-foreground" />;
|
||||||
|
case "gitlab":
|
||||||
|
return <GitlabIcon className="size-5 text-muted-foreground" />;
|
||||||
|
case "bitbucket":
|
||||||
|
return <BitbucketIcon className="size-5 text-muted-foreground" />;
|
||||||
|
case "gitea":
|
||||||
|
return <GiteaIcon className="size-5 text-muted-foreground" />;
|
||||||
|
case "git":
|
||||||
|
return <GitIcon className="size-5 text-muted-foreground" />;
|
||||||
|
default:
|
||||||
|
return <GitBranch className="size-5 text-muted-foreground" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRepositoryInfo = () => {
|
||||||
|
switch (service.sourceType) {
|
||||||
|
case "github":
|
||||||
|
return {
|
||||||
|
repo: service.repository,
|
||||||
|
branch: service.branch,
|
||||||
|
owner: service.owner,
|
||||||
|
};
|
||||||
|
case "gitlab":
|
||||||
|
return {
|
||||||
|
repo: service.gitlabRepository,
|
||||||
|
branch: service.gitlabBranch,
|
||||||
|
owner: service.gitlabOwner,
|
||||||
|
};
|
||||||
|
case "bitbucket":
|
||||||
|
return {
|
||||||
|
repo: service.bitbucketRepository,
|
||||||
|
branch: service.bitbucketBranch,
|
||||||
|
owner: service.bitbucketOwner,
|
||||||
|
};
|
||||||
|
case "gitea":
|
||||||
|
return {
|
||||||
|
repo: service.giteaRepository,
|
||||||
|
branch: service.giteaBranch,
|
||||||
|
owner: service.giteaOwner,
|
||||||
|
};
|
||||||
|
case "git":
|
||||||
|
return {
|
||||||
|
repo: service.customGitUrl,
|
||||||
|
branch: service.customGitBranch,
|
||||||
|
owner: null,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return { repo: null, branch: null, owner: null };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { repo, branch, owner } = getRepositoryInfo();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Alert>
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
This application is connected to a {service.sourceType} repository
|
||||||
|
through a git provider that you don't have access to. You can see
|
||||||
|
basic repository information below, but cannot modify the
|
||||||
|
configuration.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<Card className="border-dashed border-2 border-muted-foreground/20 bg-transparent">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
{getProviderIcon(service.sourceType)}
|
||||||
|
<span className="capitalize text-sm font-medium">
|
||||||
|
{service.sourceType} Repository
|
||||||
|
</span>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
{owner && (
|
||||||
|
<div>
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
|
Owner:
|
||||||
|
</span>
|
||||||
|
<p className="text-sm">{owner}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{repo && (
|
||||||
|
<div>
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
|
Repository:
|
||||||
|
</span>
|
||||||
|
<p className="text-sm">{repo}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{branch && (
|
||||||
|
<div>
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
|
Branch:
|
||||||
|
</span>
|
||||||
|
<p className="text-sm">{branch}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="pt-4 border-t">
|
||||||
|
<DialogAction
|
||||||
|
title="Disconnect Repository"
|
||||||
|
description="Are you sure you want to disconnect this repository?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
onDisconnect();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button variant="secondary" className="w-full">
|
||||||
|
<Unlink className="size-4 mr-2" />
|
||||||
|
Disconnect Repository
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
|
Disconnecting will allow you to configure a new repository with
|
||||||
|
your own git providers.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -68,7 +69,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
toast.success("Application deployed successfully");
|
toast.success("Application deployed successfully");
|
||||||
refetch();
|
refetch();
|
||||||
router.push(
|
router.push(
|
||||||
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`,
|
`/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/application/${applicationId}?tab=deployments`,
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user