mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-16 20:55:21 +02:00
Compare commits
683 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dadef000d5 | ||
|
|
2cda9821a5 | ||
|
|
a0868ad57c | ||
|
|
d4f574aa3f | ||
|
|
07368ff8c6 | ||
|
|
102a7a00b8 | ||
|
|
25a6a5bec6 | ||
|
|
011792e26b | ||
|
|
a527bafad8 | ||
|
|
14e154bece | ||
|
|
e5aeff6106 | ||
|
|
f6ff90eed9 | ||
|
|
f34a65cf14 | ||
|
|
8c0db75e1e | ||
|
|
b3c6645b35 | ||
|
|
0ff0695b7f | ||
|
|
6a4ef1153f | ||
|
|
a65262b45e | ||
|
|
75a66826f2 | ||
|
|
a5eeb74831 | ||
|
|
3dad8b4a54 | ||
|
|
fd94a14d85 | ||
|
|
d7e0413ed9 | ||
|
|
f4748bdd11 | ||
|
|
5a50d4bfc7 | ||
|
|
d1130c4554 | ||
|
|
fd2775e32a | ||
|
|
51003276bc | ||
|
|
6fb3584283 | ||
|
|
997dd784a5 | ||
|
|
60d1bc4a6d | ||
|
|
daa8184c30 | ||
|
|
68be333b04 | ||
|
|
2723703f60 | ||
|
|
d83783c620 | ||
|
|
ea9c76c1df | ||
|
|
32c302e9ce | ||
|
|
9f99185628 | ||
|
|
05b20193c2 | ||
|
|
88a8c060db | ||
|
|
71c01ff30f | ||
|
|
babc1c033e | ||
|
|
b34334701b | ||
|
|
0b1e79a8e1 | ||
|
|
66e0bcc4c6 | ||
|
|
962d405436 | ||
|
|
6b547dbc32 | ||
|
|
ba7a325e8f | ||
|
|
24df8e79fa | ||
|
|
6c3f72858b | ||
|
|
ba4626c7da | ||
|
|
166b58b70e | ||
|
|
74e17b4de6 | ||
|
|
7bddc6f46b | ||
|
|
036eaa3c2d | ||
|
|
be80148310 | ||
|
|
b662629075 | ||
|
|
0077954c78 | ||
|
|
622bb3ff4e | ||
|
|
7817a3c2fb | ||
|
|
71152b664b | ||
|
|
63f3bb8cf2 | ||
|
|
8510bcbd40 | ||
|
|
7fe59ba51b | ||
|
|
d858acbaaa | ||
|
|
9925470663 | ||
|
|
342b1d676e | ||
|
|
6d52ab13a6 | ||
|
|
819bb6ca14 | ||
|
|
8338b27ab8 | ||
|
|
901013ccd1 | ||
|
|
ceb4cc453e | ||
|
|
2b19632cdc | ||
|
|
a99ac01eba | ||
|
|
8b82832204 | ||
|
|
5fd39843f7 | ||
|
|
557923c011 | ||
|
|
3aaef9cc3e | ||
|
|
7a777550a0 | ||
|
|
b3cec533f9 | ||
|
|
78a9fe9dc5 | ||
|
|
68be6f451f | ||
|
|
b6e6705de8 | ||
|
|
d0fd8e7c72 | ||
|
|
b200ed6a73 | ||
|
|
8537b6fbbf | ||
|
|
883e9f0fd1 | ||
|
|
7988de64c8 | ||
|
|
fd5fa32964 | ||
|
|
ca6a93fdf6 | ||
|
|
0c37d7b3ee | ||
|
|
8de5001471 | ||
|
|
9b81d15b0c | ||
|
|
a0b550ace9 | ||
|
|
7943c90d5d | ||
|
|
fc3fceb858 | ||
|
|
1804a7c301 | ||
|
|
e97046c267 | ||
|
|
080233a7cd | ||
|
|
be5d65a8e3 | ||
|
|
e934d4f4ce | ||
|
|
586195b5c8 | ||
|
|
c8320da716 | ||
|
|
8a9a0e49ce | ||
|
|
aadb278e5f | ||
|
|
47a9bd9c86 | ||
|
|
739dc21bc0 | ||
|
|
fa4724d94e | ||
|
|
32454bab61 | ||
|
|
beb6f38204 | ||
|
|
3a0549bbd8 | ||
|
|
4112ba9b10 | ||
|
|
fbf57739b3 | ||
|
|
e4f5a1d828 | ||
|
|
3e09644877 | ||
|
|
1ab576d260 | ||
|
|
0b0f507b49 | ||
|
|
fa8722f6c8 | ||
|
|
fb0ed494fc | ||
|
|
6d2728f5f0 | ||
|
|
8efc8b573c | ||
|
|
644189064b | ||
|
|
23c891d6fc | ||
|
|
a3f9f9b7a1 | ||
|
|
83a7b8dce5 | ||
|
|
e9b5699f8e | ||
|
|
f952f53fca | ||
|
|
60db2972c7 | ||
|
|
143e4be9e6 | ||
|
|
18e553f239 | ||
|
|
c41f447269 | ||
|
|
dbc4f4e4c5 | ||
|
|
8594ad8ece | ||
|
|
9edd69b10d | ||
|
|
4a9684bbe4 | ||
|
|
4f835c6c5e | ||
|
|
54853098a7 | ||
|
|
2cc9855ed2 | ||
|
|
571e97f247 | ||
|
|
cdca2ea6d2 | ||
|
|
c4519256cf | ||
|
|
9f5c2dbe92 | ||
|
|
0f9505327f | ||
|
|
dd2902a57c | ||
|
|
0138a7c011 | ||
|
|
845d2a3ac5 | ||
|
|
4033bb84b2 | ||
|
|
43e96edcdd | ||
|
|
2db388536f | ||
|
|
43876efc79 | ||
|
|
e7c7545c02 | ||
|
|
77705381cd | ||
|
|
5fdf82a27f | ||
|
|
6bd5b1f71f | ||
|
|
17d6830b66 | ||
|
|
a845eba320 | ||
|
|
2f4ec9f35f | ||
|
|
b725861b55 | ||
|
|
6fa8f63277 | ||
|
|
ac6bdf60ec | ||
|
|
db292e6949 | ||
|
|
085f6bbbb7 | ||
|
|
cbdc4e4a20 | ||
|
|
ee3ff18feb | ||
|
|
598ecb8c6e | ||
|
|
1d5a523b9e | ||
|
|
4bced9ede0 | ||
|
|
e35aeef4e2 | ||
|
|
5e89ffbf4f | ||
|
|
21de6bf167 | ||
|
|
291edce62f | ||
|
|
59be1c5941 | ||
|
|
2141e4b174 | ||
|
|
df0fb340ad | ||
|
|
190ccfa91f | ||
|
|
f5084dd5fb | ||
|
|
cd06b55a0c | ||
|
|
b4a3cbdff4 | ||
|
|
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 | ||
|
|
65c5974b4f | ||
|
|
bdf0a932fe | ||
|
|
c355eafc95 | ||
|
|
d8a98f3936 | ||
|
|
30b28afbac | ||
|
|
c9715b19a3 | ||
|
|
1a940580ae | ||
|
|
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 | ||
|
|
b7e2df6d6a | ||
|
|
85e3a92877 | ||
|
|
c2eaa78724 | ||
|
|
270b4d4edc | ||
|
|
2d41db7f37 | ||
|
|
d0f54f2067 | ||
|
|
a6ca41f91f | ||
|
|
b2b649c5cd | ||
|
|
225c398d31 | ||
|
|
07b99bd4e4 | ||
|
|
652e8910f4 | ||
|
|
e04e25385d | ||
|
|
da9df3e239 | ||
|
|
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 | ||
|
|
e645b31b32 | ||
|
|
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 | ||
|
|
8ea64f9de1 | ||
|
|
825a1fc495 | ||
|
|
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 | ||
|
|
7b76bb93b3 | ||
|
|
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 | ||
|
|
64290fcbf6 | ||
| 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 | ||
|
|
4f2b270f1d | ||
|
|
e22489926b | ||
|
|
b4a5221caf | ||
|
|
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 |
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/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
|
||||
|
||||
- name: Get version from package.json
|
||||
id: package_version
|
||||
run: echo "VERSION=$(jq -r .version ./apps/dokploy/package.json)" >> $GITHUB_ENV
|
||||
|
||||
- name: Get latest GitHub tag
|
||||
id: latest_tag
|
||||
run: |
|
||||
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
|
||||
- name: Compare versions
|
||||
id: compare_versions
|
||||
run: |
|
||||
if [ "${{ env.VERSION }}" != "${{ env.LATEST_TAG }}" ]; then
|
||||
VERSION_CHANGED="true"
|
||||
@@ -42,7 +39,6 @@ jobs:
|
||||
echo "Latest tag: ${{ env.LATEST_TAG }}"
|
||||
echo "Version changed: $VERSION_CHANGED"
|
||||
- name: Check if a PR already exists
|
||||
id: check_pr
|
||||
run: |
|
||||
PR_EXISTS=$(gh pr list --state open --base main --head canary --json number --jq '. | length')
|
||||
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:
|
||||
push:
|
||||
branches: ["canary", "main", "feat/monitoring"]
|
||||
branches: [main, canary]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
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:
|
||||
push:
|
||||
branches: [main, canary, "1061-custom-docker-service-hostname"]
|
||||
branches: [main, canary, "fix/re-apply-database-migration-fix"]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
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
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup biomeJs
|
||||
uses: biomejs/setup-biome@v2
|
||||
|
||||
- name: Run Biome formatter
|
||||
run: biome format . --write
|
||||
run: biome format --write
|
||||
|
||||
- uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef
|
||||
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 # v1.3.2
|
||||
|
||||
39
.github/workflows/pull-request.yml
vendored
39
.github/workflows/pull-request.yml
vendored
@@ -4,9 +4,15 @@ on:
|
||||
pull_request:
|
||||
branches: [main, canary]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint-and-typecheck:
|
||||
pr-check:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
job: [build, test, typecheck]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
@@ -15,32 +21,5 @@ jobs:
|
||||
node-version: 20.16.0
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run server:build
|
||||
- run: pnpm typecheck
|
||||
|
||||
build-and-test:
|
||||
needs: lint-and-typecheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.16.0
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run server:build
|
||||
- run: pnpm build
|
||||
|
||||
parallel-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.16.0
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run server:build
|
||||
- run: pnpm test
|
||||
- run: pnpm server:build
|
||||
- run: pnpm ${{ matrix.job }}
|
||||
|
||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["biomejs.biome"]
|
||||
}
|
||||
8
.vscode/settings.json
vendored
Normal file
8
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.biome": "explicit",
|
||||
"source.organizeImports.biome": "explicit"
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,8 @@ pnpm run dokploy:dev
|
||||
|
||||
Go to http://localhost:3000 to see the development server
|
||||
|
||||
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
|
||||
|
||||
@@ -117,10 +118,10 @@ In the case you lost your password, you can reset it using the following command
|
||||
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
|
||||
bunx lt --port 3000
|
||||
pnpm dlx localtunnel --port 3000
|
||||
```
|
||||
|
||||
If you run into permission issues of docker run the following command
|
||||
@@ -152,7 +153,7 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.
|
||||
|
||||
## Pull Request
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
@@ -161,6 +162,12 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.
|
||||
- If your pull request fixes an open issue, please reference the issue in the pull request description.
|
||||
- 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!
|
||||
|
||||
## Templates
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:20.9-slim AS base
|
||||
FROM node:20.16.0-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
RUN corepack prepare pnpm@9.12.0 --activate
|
||||
|
||||
FROM base AS build
|
||||
COPY . /usr/src/app
|
||||
@@ -57,7 +58,7 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
||||
&& pnpm install -g tsx
|
||||
|
||||
# Install Railpack
|
||||
ARG RAILPACK_VERSION=0.0.64
|
||||
ARG RAILPACK_VERSION=0.2.2
|
||||
RUN curl -sSL https://railpack.com/install.sh | bash
|
||||
|
||||
# Install buildpacks
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:20.9-slim AS base
|
||||
FROM node:20.16.0-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
RUN corepack prepare pnpm@9.12.0 --activate
|
||||
|
||||
FROM base AS build
|
||||
COPY . /usr/src/app
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:20.9-slim AS base
|
||||
FROM node:20.16.0-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
RUN corepack prepare pnpm@9.12.0 --activate
|
||||
|
||||
FROM base AS build
|
||||
COPY . /usr/src/app
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:20.9-slim AS base
|
||||
FROM node:20.16.0-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
RUN corepack prepare pnpm@9.12.0 --activate
|
||||
|
||||
FROM base AS build
|
||||
COPY . /usr/src/app
|
||||
|
||||
49
README.md
49
README.md
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
<a href="https://dokploy.com">
|
||||
<img src=".github/sponsors/logo.png" alt="Dokploy - Open Source Alternative to Vercel, Heroku and Netlify." align="center" width="100%" />
|
||||
<img src=".github/sponsors/logo.png" alt="Dokploy - Open Source Alternative to Vercel, Heroku and Netlify." width="100%" />
|
||||
</a>
|
||||
</br>
|
||||
</br>
|
||||
@@ -11,9 +11,26 @@
|
||||
</div>
|
||||
<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.
|
||||
|
||||
### Features
|
||||
|
||||
## ✨ Features
|
||||
|
||||
Dokploy includes multiple features to make your life easier.
|
||||
|
||||
@@ -43,7 +60,7 @@ curl -sSL https://dokploy.com/install.sh | sh
|
||||
|
||||
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.
|
||||
|
||||
@@ -60,8 +77,10 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
<div>
|
||||
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy"><img src=".github/sponsors/hostinger.jpg" alt="Hostinger" width="300"/></a>
|
||||
<a href="https://www.lxaer.com/?ref=dokploy"><img src=".github/sponsors/lxaer.png" alt="LX Aer" width="100"/></a>
|
||||
<a href="https://mandarin3d.com/?ref=dokploy"><img src=".github/sponsors/mandarin.png" alt="Mandarin" width="100"/></a>
|
||||
<a href="https://lightnode.com/?ref=dokploy"><img src=".github/sponsors/light-node.webp" alt="Lightnode" width="300"/></a>
|
||||
<a href="https://www.lambdatest.com/?utm_source=dokploy&utm_medium=sponsor" target="_blank">
|
||||
<img src="https://www.lambdatest.com/blue-logo.png" width="450" height="100" />
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Premium Supporters 🥇 -->
|
||||
@@ -89,25 +108,17 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
### Supporting Members 🥉
|
||||
|
||||
<div>
|
||||
<a href="https://lightspeed.run/?ref=dokploy"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
|
||||
|
||||
<a href="https://cloudblast.io/?ref=dokploy"><img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" width="250px" alt="Cloudblast.io"/></a>
|
||||
<a href="https://startupfa.me/?ref=dokploy"><img src=".github/sponsors/startupfame.png" width="65px" alt="Startupfame"/></a>
|
||||
<a href="https://itsdb-center.com?ref=dokploy"><img src=".github/sponsors/its.png" width="65px" alt="Itsdb-center"/></a>
|
||||
<a href="https://openalternative.co/?ref=dokploy"><img src=".github/sponsors/openalternative.png" width="65px" alt="Openalternative"/></a>
|
||||
|
||||
<a href="https://synexa.ai/?ref=dokploy"><img src=".github/sponsors/synexa.png" width="65px" alt="Synexa"/></a>
|
||||
</div>
|
||||
|
||||
### Community Backers 🤝
|
||||
|
||||
<div>
|
||||
<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:
|
||||
|
||||
[](https://opencollective.com/dokploy)
|
||||
[Sponsors on Open Collective](https://opencollective.com/dokploy)
|
||||
|
||||
#### Individuals:
|
||||
|
||||
@@ -116,15 +127,15 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
### 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
|
||||
## 📺 Video Tutorial
|
||||
|
||||
<a href="https://youtu.be/mznYKPvhcfw">
|
||||
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400"/>
|
||||
</a>
|
||||
|
||||
## Contributing
|
||||
## 🤝 Contributing
|
||||
|
||||
Check out the [Contributing Guide](CONTRIBUTING.md) for more information.
|
||||
|
||||
@@ -9,25 +9,30 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"inngest": "3.40.1",
|
||||
"@dokploy/server": "workspace:*",
|
||||
"@hono/node-server": "^1.12.1",
|
||||
"@hono/node-server": "^1.14.3",
|
||||
"@hono/zod-validator": "0.3.0",
|
||||
"@nerimity/mimiqueue": "1.2.3",
|
||||
"dotenv": "^16.3.1",
|
||||
"hono": "^4.5.8",
|
||||
"dotenv": "^16.4.5",
|
||||
"hono": "^4.7.10",
|
||||
"pino": "9.4.0",
|
||||
"pino-pretty": "11.2.2",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"redis": "4.7.0",
|
||||
"zod": "^3.23.4"
|
||||
"zod": "^3.25.32"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.17",
|
||||
"@types/node": "^20.17.51",
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.4.2"
|
||||
"tsx": "^4.16.2",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"packageManager": "pnpm@9.5.0"
|
||||
"packageManager": "pnpm@9.12.0",
|
||||
"engines": {
|
||||
"node": "^20.16.0",
|
||||
"pnpm": ">=9.12.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,21 +2,90 @@ import { serve } from "@hono/node-server";
|
||||
import { Hono } from "hono";
|
||||
import "dotenv/config";
|
||||
import { zValidator } from "@hono/zod-validator";
|
||||
import { Queue } from "@nerimity/mimiqueue";
|
||||
import { createClient } from "redis";
|
||||
import { Inngest } from "inngest";
|
||||
import { serve as serveInngest } from "inngest/hono";
|
||||
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";
|
||||
|
||||
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) => {
|
||||
if (c.req.path === "/health") {
|
||||
if (c.req.path === "/health" || c.req.path === "/api/inngest") {
|
||||
return next();
|
||||
}
|
||||
|
||||
const authHeader = c.req.header("X-API-Key");
|
||||
|
||||
if (process.env.API_KEY !== authHeader) {
|
||||
@@ -26,36 +95,97 @@ app.use(async (c, next) => {
|
||||
return next();
|
||||
});
|
||||
|
||||
app.post("/deploy", zValidator("json", deployJobSchema), (c) => {
|
||||
app.post("/deploy", zValidator("json", deployJobSchema), async (c) => {
|
||||
const data = c.req.valid("json");
|
||||
queue.add(data, { groupName: data.serverId });
|
||||
return c.json(
|
||||
{
|
||||
message: "Deployment Added",
|
||||
},
|
||||
200,
|
||||
);
|
||||
logger.info("Received deployment request", data);
|
||||
|
||||
try {
|
||||
// Send event to Inngest instead of adding to Redis queue
|
||||
await inngest.send({
|
||||
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) => {
|
||||
return c.json({ status: "ok" });
|
||||
});
|
||||
|
||||
const queue = new Queue({
|
||||
name: "deployments",
|
||||
process: async (job: DeployJob) => {
|
||||
logger.info("Deploying job", job);
|
||||
return await deploy(job);
|
||||
},
|
||||
redisClient,
|
||||
});
|
||||
|
||||
(async () => {
|
||||
await redisClient.connect();
|
||||
await redisClient.flushAll();
|
||||
logger.info("Redis Cleaned");
|
||||
})();
|
||||
// Serve Inngest functions endpoint
|
||||
app.on(
|
||||
["GET", "POST", "PUT"],
|
||||
"/api/inngest",
|
||||
serveInngest({
|
||||
client: inngest,
|
||||
functions: [deploymentFunction],
|
||||
}),
|
||||
);
|
||||
|
||||
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 });
|
||||
|
||||
@@ -3,8 +3,8 @@ import { z } from "zod";
|
||||
export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||
z.object({
|
||||
applicationId: z.string(),
|
||||
titleLog: z.string(),
|
||||
descriptionLog: z.string(),
|
||||
titleLog: z.string().optional(),
|
||||
descriptionLog: z.string().optional(),
|
||||
server: z.boolean().optional(),
|
||||
type: z.enum(["deploy", "redeploy"]),
|
||||
applicationType: z.literal("application"),
|
||||
@@ -12,8 +12,8 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||
}),
|
||||
z.object({
|
||||
composeId: z.string(),
|
||||
titleLog: z.string(),
|
||||
descriptionLog: z.string(),
|
||||
titleLog: z.string().optional(),
|
||||
descriptionLog: z.string().optional(),
|
||||
server: z.boolean().optional(),
|
||||
type: z.enum(["deploy", "redeploy"]),
|
||||
applicationType: z.literal("compose"),
|
||||
@@ -22,8 +22,8 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||
z.object({
|
||||
applicationId: z.string(),
|
||||
previewDeploymentId: z.string(),
|
||||
titleLog: z.string(),
|
||||
descriptionLog: z.string(),
|
||||
titleLog: z.string().optional(),
|
||||
descriptionLog: z.string().optional(),
|
||||
server: z.boolean().optional(),
|
||||
type: z.enum(["deploy"]),
|
||||
applicationType: z.literal("application-preview"),
|
||||
@@ -32,3 +32,16 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||
]);
|
||||
|
||||
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") {
|
||||
await rebuildRemoteApplication({
|
||||
applicationId: job.applicationId,
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
titleLog: job.titleLog || "Rebuild deployment",
|
||||
descriptionLog: job.descriptionLog || "",
|
||||
});
|
||||
} else if (job.type === "deploy") {
|
||||
await deployRemoteApplication({
|
||||
applicationId: job.applicationId,
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
titleLog: job.titleLog || "Manual deployment",
|
||||
descriptionLog: job.descriptionLog || "",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -38,14 +38,14 @@ export const deploy = async (job: DeployJob) => {
|
||||
if (job.type === "redeploy") {
|
||||
await rebuildRemoteCompose({
|
||||
composeId: job.composeId,
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
titleLog: job.titleLog || "Rebuild deployment",
|
||||
descriptionLog: job.descriptionLog || "",
|
||||
});
|
||||
} else if (job.type === "deploy") {
|
||||
await deployRemoteCompose({
|
||||
composeId: job.composeId,
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
titleLog: job.titleLog || "Manual deployment",
|
||||
descriptionLog: job.descriptionLog || "",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -57,14 +57,14 @@ export const deploy = async (job: DeployJob) => {
|
||||
if (job.type === "deploy") {
|
||||
await deployRemotePreviewApplication({
|
||||
applicationId: job.applicationId,
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
titleLog: job.titleLog || "Preview Deployment",
|
||||
descriptionLog: job.descriptionLog || "",
|
||||
previewDeploymentId: job.previewDeploymentId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
} catch (e) {
|
||||
if (job.applicationType === "application") {
|
||||
await updateApplicationStatus(job.applicationId, "error");
|
||||
} else if (job.applicationType === "compose") {
|
||||
@@ -76,6 +76,8 @@ export const deploy = async (job: DeployJob) => {
|
||||
previewStatus: "error",
|
||||
});
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { addSuffixToAllProperties } 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 { parse } from "yaml";
|
||||
|
||||
const composeFile1 = `
|
||||
version: "3.8"
|
||||
@@ -61,7 +61,7 @@ secrets:
|
||||
file: ./db_password.txt
|
||||
`;
|
||||
|
||||
const expectedComposeFile1 = load(`
|
||||
const expectedComposeFile1 = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -120,7 +120,7 @@ secrets:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
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 updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
||||
@@ -185,7 +185,7 @@ secrets:
|
||||
file: ./db_password.txt
|
||||
`;
|
||||
|
||||
const expectedComposeFile2 = load(`
|
||||
const expectedComposeFile2 = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -243,7 +243,7 @@ secrets:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
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 updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
||||
@@ -308,7 +308,7 @@ secrets:
|
||||
file: ./service_secret.txt
|
||||
`;
|
||||
|
||||
const expectedComposeFile3 = load(`
|
||||
const expectedComposeFile3 = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -366,7 +366,7 @@ secrets:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
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 updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
||||
@@ -420,7 +420,7 @@ volumes:
|
||||
driver: local
|
||||
`;
|
||||
|
||||
const expectedComposeFile = load(`
|
||||
const expectedComposeFile = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -467,7 +467,7 @@ volumes:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
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 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 { load } from "js-yaml";
|
||||
import { addSuffixToConfigsRoot, generateRandomHash } from "@dokploy/server";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
@@ -24,7 +23,7 @@ configs:
|
||||
`;
|
||||
|
||||
test("Add suffix to configs in root property", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
const composeData = parse(composeFile) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -60,7 +59,7 @@ configs:
|
||||
`;
|
||||
|
||||
test("Add suffix to multiple configs in root property", () => {
|
||||
const composeData = load(composeFileMultipleConfigs) as ComposeSpecification;
|
||||
const composeData = parse(composeFileMultipleConfigs) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -93,7 +92,7 @@ configs:
|
||||
`;
|
||||
|
||||
test("Add suffix to configs with different properties in root property", () => {
|
||||
const composeData = load(
|
||||
const composeData = parse(
|
||||
composeFileDifferentProperties,
|
||||
) as ComposeSpecification;
|
||||
|
||||
@@ -138,7 +137,7 @@ configs:
|
||||
`;
|
||||
|
||||
// Expected compose file con el prefijo `testhash`
|
||||
const expectedComposeFileConfigRoot = load(`
|
||||
const expectedComposeFileConfigRoot = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -163,7 +162,7 @@ configs:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to configs in root property", () => {
|
||||
const composeData = load(composeFileConfigRoot) as ComposeSpecification;
|
||||
const composeData = parse(composeFileConfigRoot) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToConfigsInServices } 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 { parse } from "yaml";
|
||||
|
||||
const composeFile = `
|
||||
version: "3.8"
|
||||
@@ -20,7 +22,7 @@ configs:
|
||||
`;
|
||||
|
||||
test("Add suffix to configs in services", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
const composeData = parse(composeFile) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -52,7 +54,7 @@ configs:
|
||||
`;
|
||||
|
||||
test("Add suffix to configs in services with single config", () => {
|
||||
const composeData = load(
|
||||
const composeData = parse(
|
||||
composeFileSingleServiceConfig,
|
||||
) as ComposeSpecification;
|
||||
|
||||
@@ -106,7 +108,7 @@ configs:
|
||||
`;
|
||||
|
||||
test("Add suffix to configs in services with multiple configs", () => {
|
||||
const composeData = load(
|
||||
const composeData = parse(
|
||||
composeFileMultipleServicesConfigs,
|
||||
) as ComposeSpecification;
|
||||
|
||||
@@ -155,7 +157,7 @@ services:
|
||||
`;
|
||||
|
||||
// Expected compose file con el prefijo `testhash`
|
||||
const expectedComposeFileConfigServices = load(`
|
||||
const expectedComposeFileConfigServices = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -180,7 +182,7 @@ services:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to configs in services", () => {
|
||||
const composeData = load(composeFileConfigServices) as ComposeSpecification;
|
||||
const composeData = parse(composeFileConfigServices) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToAllConfigs } 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 { parse } from "yaml";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
@@ -44,7 +43,7 @@ configs:
|
||||
file: ./db-config.yml
|
||||
`;
|
||||
|
||||
const expectedComposeFileCombinedConfigs = load(`
|
||||
const expectedComposeFileCombinedConfigs = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -78,7 +77,7 @@ configs:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
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";
|
||||
|
||||
@@ -123,7 +122,7 @@ configs:
|
||||
file: ./db-config.yml
|
||||
`;
|
||||
|
||||
const expectedComposeFileWithEnvAndExternal = load(`
|
||||
const expectedComposeFileWithEnvAndExternal = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -160,7 +159,7 @@ configs:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to configs with environment and external", () => {
|
||||
const composeData = load(
|
||||
const composeData = parse(
|
||||
composeFileWithEnvAndExternal,
|
||||
) as ComposeSpecification;
|
||||
|
||||
@@ -201,7 +200,7 @@ configs:
|
||||
file: ./db-config.yml
|
||||
`;
|
||||
|
||||
const expectedComposeFileWithTemplateDriverAndLabels = load(`
|
||||
const expectedComposeFileWithTemplateDriverAndLabels = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -232,7 +231,7 @@ configs:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to configs with template driver and labels", () => {
|
||||
const composeData = load(
|
||||
const composeData = parse(
|
||||
composeFileWithTemplateDriverAndLabels,
|
||||
) as ComposeSpecification;
|
||||
|
||||
|
||||
@@ -108,4 +108,136 @@ describe("createDomainLabels", () => {
|
||||
"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 { load } from "js-yaml";
|
||||
import { addSuffixToNetworksRoot, generateRandomHash } from "@dokploy/server";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
const composeFile = `
|
||||
version: "3.8"
|
||||
@@ -36,7 +35,7 @@ test("Generate random hash with 8 characters", () => {
|
||||
});
|
||||
|
||||
test("Add suffix to networks root property", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
const composeData = parse(composeFile) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -80,7 +79,7 @@ networks:
|
||||
`;
|
||||
|
||||
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();
|
||||
|
||||
@@ -121,7 +120,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to networks with external properties", () => {
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -161,7 +160,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to networks with IPAM configurations", () => {
|
||||
const composeData = load(composeFile4) as ComposeSpecification;
|
||||
const composeData = parse(composeFile4) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -202,7 +201,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to networks with custom options", () => {
|
||||
const composeData = load(composeFile5) as ComposeSpecification;
|
||||
const composeData = parse(composeFile5) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -265,7 +264,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to networks with static suffix", () => {
|
||||
const composeData = load(composeFile6) as ComposeSpecification;
|
||||
const composeData = parse(composeFile6) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
|
||||
@@ -274,7 +273,7 @@ test("Add suffix to networks with static suffix", () => {
|
||||
}
|
||||
const networks = addSuffixToNetworksRoot(composeData.networks, suffix);
|
||||
|
||||
const expectedComposeData = load(
|
||||
const expectedComposeData = parse(
|
||||
expectedComposeFile6,
|
||||
) as ComposeSpecification;
|
||||
expect(networks).toStrictEqual(expectedComposeData.networks);
|
||||
@@ -294,7 +293,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("It shoudn't add suffix to dokploy-network", () => {
|
||||
const composeData = load(composeFile7) as ComposeSpecification;
|
||||
const composeData = parse(composeFile7) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNetworks } 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 { parse } from "yaml";
|
||||
|
||||
const composeFile = `
|
||||
version: "3.8"
|
||||
@@ -21,7 +23,7 @@ services:
|
||||
`;
|
||||
|
||||
test("Add suffix to networks in services", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
const composeData = parse(composeFile) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -65,7 +67,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to networks in services with aliases", () => {
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -105,7 +107,7 @@ 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();
|
||||
|
||||
@@ -151,7 +153,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to networks in services (combined case)", () => {
|
||||
const composeData = load(composeFileCombined) as ComposeSpecification;
|
||||
const composeData = parse(composeFileCombined) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -194,7 +196,7 @@ 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();
|
||||
|
||||
@@ -243,7 +245,7 @@ services:
|
||||
`;
|
||||
|
||||
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();
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import {
|
||||
addSuffixToAllNetworks,
|
||||
addSuffixToNetworksRoot,
|
||||
addSuffixToServiceNetworks,
|
||||
generateRandomHash,
|
||||
} 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 { parse } from "yaml";
|
||||
|
||||
const composeFileCombined = `
|
||||
version: "3.8"
|
||||
@@ -39,7 +39,7 @@ networks:
|
||||
`;
|
||||
|
||||
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();
|
||||
|
||||
@@ -89,7 +89,7 @@ test("Add suffix to networks in services and root (combined case)", () => {
|
||||
expect(redisNetworks).not.toHaveProperty("backend");
|
||||
});
|
||||
|
||||
const expectedComposeFile = load(`
|
||||
const expectedComposeFile = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -120,7 +120,7 @@ networks:
|
||||
`);
|
||||
|
||||
test("Add suffix to networks in compose file", () => {
|
||||
const composeData = load(composeFileCombined) as ComposeSpecification;
|
||||
const composeData = parse(composeFileCombined) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
if (!composeData?.networks) {
|
||||
@@ -156,7 +156,7 @@ networks:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
const expectedComposeFile2 = load(`
|
||||
const expectedComposeFile2 = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -182,7 +182,7 @@ 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 updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
|
||||
@@ -218,7 +218,7 @@ networks:
|
||||
com.docker.network.bridge.enable_icc: "true"
|
||||
`;
|
||||
|
||||
const expectedComposeFile3 = load(`
|
||||
const expectedComposeFile3 = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -247,7 +247,7 @@ networks:
|
||||
`);
|
||||
|
||||
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 updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
|
||||
@@ -289,7 +289,7 @@ networks:
|
||||
|
||||
`;
|
||||
|
||||
const expectedComposeFile4 = load(`
|
||||
const expectedComposeFile4 = parse(`
|
||||
version: "3.8"
|
||||
|
||||
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", () => {
|
||||
const composeData = load(composeFile4) as ComposeSpecification;
|
||||
const composeData = parse(composeFile4) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
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 { load } from "js-yaml";
|
||||
import { addSuffixToSecretsRoot, generateRandomHash } from "@dokploy/server";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
@@ -24,7 +23,7 @@ secrets:
|
||||
`;
|
||||
|
||||
test("Add suffix to secrets in root property", () => {
|
||||
const composeData = load(composeFileSecretsRoot) as ComposeSpecification;
|
||||
const composeData = parse(composeFileSecretsRoot) as ComposeSpecification;
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
if (!composeData?.secrets) {
|
||||
@@ -53,7 +52,7 @@ secrets:
|
||||
`;
|
||||
|
||||
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();
|
||||
|
||||
if (!composeData?.secrets) {
|
||||
@@ -85,7 +84,7 @@ secrets:
|
||||
`;
|
||||
|
||||
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();
|
||||
|
||||
if (!composeData?.secrets) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToSecretsInServices } 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 { parse } from "yaml";
|
||||
|
||||
const composeFileSecretsServices = `
|
||||
version: "3.8"
|
||||
@@ -19,7 +21,7 @@ secrets:
|
||||
`;
|
||||
|
||||
test("Add suffix to secrets in services", () => {
|
||||
const composeData = load(composeFileSecretsServices) as ComposeSpecification;
|
||||
const composeData = parse(composeFileSecretsServices) as ComposeSpecification;
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
@@ -52,7 +54,9 @@ secrets:
|
||||
`;
|
||||
|
||||
test("Add suffix to secrets in services (Test 1)", () => {
|
||||
const composeData = load(composeFileSecretsServices1) as ComposeSpecification;
|
||||
const composeData = parse(
|
||||
composeFileSecretsServices1,
|
||||
) as ComposeSpecification;
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
@@ -91,7 +95,9 @@ secrets:
|
||||
`;
|
||||
|
||||
test("Add suffix to secrets in services (Test 2)", () => {
|
||||
const composeData = load(composeFileSecretsServices2) as ComposeSpecification;
|
||||
const composeData = parse(
|
||||
composeFileSecretsServices2,
|
||||
) as ComposeSpecification;
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { addSuffixToAllSecrets } 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 { parse } from "yaml";
|
||||
|
||||
const composeFileCombinedSecrets = `
|
||||
version: "3.8"
|
||||
@@ -25,7 +25,7 @@ secrets:
|
||||
file: ./app_secret.txt
|
||||
`;
|
||||
|
||||
const expectedComposeFileCombinedSecrets = load(`
|
||||
const expectedComposeFileCombinedSecrets = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -48,7 +48,7 @@ secrets:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all secrets", () => {
|
||||
const composeData = load(composeFileCombinedSecrets) as ComposeSpecification;
|
||||
const composeData = parse(composeFileCombinedSecrets) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
|
||||
@@ -77,7 +77,7 @@ secrets:
|
||||
file: ./cache_secret.txt
|
||||
`;
|
||||
|
||||
const expectedComposeFileCombinedSecrets3 = load(`
|
||||
const expectedComposeFileCombinedSecrets3 = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -99,7 +99,9 @@ secrets:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all secrets (3rd Case)", () => {
|
||||
const composeData = load(composeFileCombinedSecrets3) as ComposeSpecification;
|
||||
const composeData = parse(
|
||||
composeFileCombinedSecrets3,
|
||||
) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
|
||||
@@ -128,7 +130,7 @@ secrets:
|
||||
file: ./db_password.txt
|
||||
`;
|
||||
|
||||
const expectedComposeFileCombinedSecrets4 = load(`
|
||||
const expectedComposeFileCombinedSecrets4 = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -150,7 +152,9 @@ secrets:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all secrets (4th Case)", () => {
|
||||
const composeData = load(composeFileCombinedSecrets4) as ComposeSpecification;
|
||||
const composeData = parse(
|
||||
composeFileCombinedSecrets4,
|
||||
) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
|
||||
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 { load } from "js-yaml";
|
||||
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
const composeFile = `
|
||||
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", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
const composeData = parse(composeFile) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames } 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 { parse } from "yaml";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
@@ -33,7 +32,7 @@ networks:
|
||||
`;
|
||||
|
||||
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();
|
||||
|
||||
@@ -103,7 +102,7 @@ networks:
|
||||
`;
|
||||
|
||||
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();
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames } 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 { parse } from "yaml";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
@@ -31,7 +30,7 @@ networks:
|
||||
`;
|
||||
|
||||
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();
|
||||
|
||||
@@ -91,7 +90,7 @@ networks:
|
||||
`;
|
||||
|
||||
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();
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames } 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 { parse } from "yaml";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
@@ -32,7 +31,7 @@ networks:
|
||||
`;
|
||||
|
||||
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();
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames } 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 { parse } from "yaml";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
@@ -27,7 +26,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to service names in compose file", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
const composeData = parse(composeFile) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import {
|
||||
addSuffixToAllServiceNames,
|
||||
addSuffixToServiceNames,
|
||||
} from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
const composeFileCombinedAllCases = `
|
||||
version: "3.8"
|
||||
@@ -38,7 +38,7 @@ networks:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
const expectedComposeFile = load(`
|
||||
const expectedComposeFile = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -71,7 +71,9 @@ networks:
|
||||
`);
|
||||
|
||||
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";
|
||||
|
||||
@@ -131,7 +133,7 @@ networks:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
const expectedComposeFile1 = load(`
|
||||
const expectedComposeFile1 = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -176,7 +178,7 @@ networks:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
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 updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
|
||||
@@ -227,7 +229,7 @@ networks:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
const expectedComposeFile2 = load(`
|
||||
const expectedComposeFile2 = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -271,7 +273,7 @@ networks:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
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 updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
|
||||
@@ -322,7 +324,7 @@ networks:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
const expectedComposeFile3 = load(`
|
||||
const expectedComposeFile3 = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -366,7 +368,7 @@ networks:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
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 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 { load } from "js-yaml";
|
||||
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
@@ -36,7 +35,7 @@ networks:
|
||||
`;
|
||||
|
||||
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();
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToAllVolumes, addSuffixToVolumesRoot } 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 { parse } from "yaml";
|
||||
|
||||
const composeFile = `
|
||||
services:
|
||||
@@ -67,7 +70,7 @@ volumes:
|
||||
driver: local
|
||||
`;
|
||||
|
||||
const expectedDockerCompose = load(`
|
||||
const expectedDockerCompose = parse(`
|
||||
services:
|
||||
mail:
|
||||
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
|
||||
// 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", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
const composeData = parse(composeFile) as ComposeSpecification;
|
||||
|
||||
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", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
const composeData = parse(composeFile) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||
@@ -192,7 +195,7 @@ volumes:
|
||||
mongo-data:
|
||||
`;
|
||||
|
||||
const expectedDockerCompose2 = load(`
|
||||
const expectedDockerCompose2 = parse(`
|
||||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
@@ -215,7 +218,7 @@ volumes:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
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 updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||
@@ -245,7 +248,7 @@ volumes:
|
||||
mongo-data:
|
||||
`;
|
||||
|
||||
const expectedDockerCompose3 = load(`
|
||||
const expectedDockerCompose3 = parse(`
|
||||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
@@ -268,7 +271,7 @@ volumes:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
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 updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||
@@ -642,7 +645,7 @@ volumes:
|
||||
db-config:
|
||||
`;
|
||||
|
||||
const expectedDockerComposeComplex = load(`
|
||||
const expectedDockerComposeComplex = parse(`
|
||||
version: "3.8"
|
||||
services:
|
||||
studio:
|
||||
@@ -1009,7 +1012,7 @@ volumes:
|
||||
`);
|
||||
|
||||
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 updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||
@@ -1062,7 +1065,7 @@ volumes:
|
||||
db-data:
|
||||
`;
|
||||
|
||||
const expectedDockerComposeExample1 = load(`
|
||||
const expectedDockerComposeExample1 = parse(`
|
||||
version: "3.8"
|
||||
services:
|
||||
web:
|
||||
@@ -1108,7 +1111,7 @@ volumes:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
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 updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||
@@ -1140,7 +1143,7 @@ volumes:
|
||||
backrest-cache:
|
||||
`;
|
||||
|
||||
const expectedDockerComposeBackrest = load(`
|
||||
const expectedDockerComposeBackrest = parse(`
|
||||
services:
|
||||
backrest:
|
||||
image: garethgeorge/backrest:v1.7.3
|
||||
@@ -1165,7 +1168,7 @@ volumes:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Should handle volume paths with subdirectories correctly", () => {
|
||||
const composeData = load(composeFileBackrest) as ComposeSpecification;
|
||||
const composeData = parse(composeFileBackrest) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
|
||||
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 { load } from "js-yaml";
|
||||
import { addSuffixToVolumesRoot, generateRandomHash } from "@dokploy/server";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
const composeFile = `
|
||||
version: "3.8"
|
||||
@@ -30,7 +29,7 @@ test("Generate random hash with 8 characters", () => {
|
||||
});
|
||||
|
||||
test("Add suffix to volumes in root property", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
const composeData = parse(composeFile) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -68,7 +67,7 @@ networks:
|
||||
`;
|
||||
|
||||
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();
|
||||
|
||||
@@ -102,7 +101,7 @@ networks:
|
||||
`;
|
||||
|
||||
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();
|
||||
|
||||
@@ -149,7 +148,7 @@ volumes:
|
||||
`;
|
||||
|
||||
// Expected compose file con el prefijo `testhash`
|
||||
const expectedComposeFile4 = load(`
|
||||
const expectedComposeFile4 = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -180,7 +179,7 @@ volumes:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to volumes in root property", () => {
|
||||
const composeData = load(composeFile4) as ComposeSpecification;
|
||||
const composeData = parse(composeFile4) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToVolumesInServices } 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 { parse } from "yaml";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
@@ -22,7 +24,7 @@ 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();
|
||||
|
||||
@@ -57,7 +59,7 @@ volumes:
|
||||
`;
|
||||
|
||||
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();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { addSuffixToAllVolumes } 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 { parse } from "yaml";
|
||||
|
||||
const composeFileTypeVolume = `
|
||||
version: "3.8"
|
||||
@@ -23,7 +23,7 @@ volumes:
|
||||
driver: local
|
||||
`;
|
||||
|
||||
const expectedComposeFileTypeVolume = load(`
|
||||
const expectedComposeFileTypeVolume = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -44,7 +44,7 @@ volumes:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
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";
|
||||
|
||||
@@ -73,7 +73,7 @@ volumes:
|
||||
driver: local
|
||||
`;
|
||||
|
||||
const expectedComposeFileTypeVolume1 = load(`
|
||||
const expectedComposeFileTypeVolume1 = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -93,7 +93,7 @@ volumes:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to mixed volumes in services", () => {
|
||||
const composeData = load(composeFileTypeVolume1) as ComposeSpecification;
|
||||
const composeData = parse(composeFileTypeVolume1) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
|
||||
@@ -128,7 +128,7 @@ volumes:
|
||||
device: /path/to/app/logs
|
||||
`;
|
||||
|
||||
const expectedComposeFileTypeVolume2 = load(`
|
||||
const expectedComposeFileTypeVolume2 = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -154,7 +154,7 @@ volumes:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to complex volume configurations in services", () => {
|
||||
const composeData = load(composeFileTypeVolume2) as ComposeSpecification;
|
||||
const composeData = parse(composeFileTypeVolume2) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
|
||||
@@ -218,7 +218,7 @@ volumes:
|
||||
device: /path/to/shared/logs
|
||||
`;
|
||||
|
||||
const expectedComposeFileTypeVolume3 = load(`
|
||||
const expectedComposeFileTypeVolume3 = parse(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -273,7 +273,7 @@ volumes:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
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";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]";
|
||||
|
||||
describe("GitHub Webhook Skip CI", () => {
|
||||
const mockGithubHeaders = {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
const { APPLICATIONS_PATH } = paths();
|
||||
import type { ApplicationNested } from "@dokploy/server";
|
||||
import { unzipDrop } from "@dokploy/server";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import AdmZip from "adm-zip";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { APPLICATIONS_PATH } = paths();
|
||||
vi.mock("@dokploy/server/constants", async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
@@ -25,10 +25,13 @@ if (typeof window === "undefined") {
|
||||
}
|
||||
|
||||
const baseApp: ApplicationNested = {
|
||||
railpackVersion: "0.2.2",
|
||||
applicationId: "",
|
||||
previewLabels: [],
|
||||
herokuVersion: "",
|
||||
giteaBranch: "",
|
||||
giteaBuildPath: "",
|
||||
previewRequireCollaboratorPermissions: false,
|
||||
giteaId: "",
|
||||
giteaOwner: "",
|
||||
giteaRepository: "",
|
||||
@@ -45,6 +48,7 @@ const baseApp: ApplicationNested = {
|
||||
dockerBuildStage: "",
|
||||
isPreviewDeploymentsActive: false,
|
||||
previewBuildArgs: null,
|
||||
previewBuildSecrets: null,
|
||||
previewCertificateType: "none",
|
||||
previewCustomCertResolver: null,
|
||||
previewEnv: null,
|
||||
@@ -53,15 +57,24 @@ const baseApp: ApplicationNested = {
|
||||
previewPort: 3000,
|
||||
previewLimit: 0,
|
||||
previewWildcard: "",
|
||||
project: {
|
||||
environment: {
|
||||
env: "",
|
||||
organizationId: "",
|
||||
environmentId: "",
|
||||
name: "",
|
||||
description: "",
|
||||
createdAt: "",
|
||||
description: "",
|
||||
projectId: "",
|
||||
project: {
|
||||
env: "",
|
||||
organizationId: "",
|
||||
name: "",
|
||||
description: "",
|
||||
createdAt: "",
|
||||
projectId: "",
|
||||
},
|
||||
},
|
||||
buildArgs: null,
|
||||
buildSecrets: null,
|
||||
buildPath: "/",
|
||||
gitlabPathNamespace: "",
|
||||
buildType: "nixpacks",
|
||||
@@ -89,6 +102,7 @@ const baseApp: ApplicationNested = {
|
||||
dockerfile: null,
|
||||
dockerImage: null,
|
||||
dropBuildPath: null,
|
||||
environmentId: "",
|
||||
enabled: null,
|
||||
env: null,
|
||||
healthCheckSwarm: null,
|
||||
@@ -103,7 +117,6 @@ const baseApp: ApplicationNested = {
|
||||
password: null,
|
||||
placementSwarm: null,
|
||||
ports: [],
|
||||
projectId: "",
|
||||
publishDirectory: null,
|
||||
isStaticSpa: null,
|
||||
redirects: [],
|
||||
@@ -122,6 +135,7 @@ const baseApp: ApplicationNested = {
|
||||
username: null,
|
||||
dockerContextPath: null,
|
||||
rollbackActive: false,
|
||||
stopGracePeriodSwarm: null,
|
||||
};
|
||||
|
||||
describe("unzipDrop using real zip files", () => {
|
||||
@@ -141,7 +155,7 @@ describe("unzipDrop using real zip files", () => {
|
||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
|
||||
console.log(`Output Path: ${outputPath}`);
|
||||
const zipBuffer = zip.toBuffer();
|
||||
const zipBuffer = zip.toBuffer() as Buffer<ArrayBuffer>;
|
||||
const file = new File([zipBuffer], "single.zip");
|
||||
await unzipDrop(file, baseApp);
|
||||
const files = await fs.readdir(outputPath, { withFileTypes: 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 { 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"}`;
|
||||
|
||||
describe("processLogs", () => {
|
||||
|
||||
102
apps/dokploy/__test__/server/mechanizeDockerContainer.test.ts
Normal file
102
apps/dokploy/__test__/server/mechanizeDockerContainer.test.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { ApplicationNested } from "@dokploy/server/utils/builders";
|
||||
import { mechanizeDockerContainer } from "@dokploy/server/utils/builders";
|
||||
|
||||
type MockCreateServiceOptions = {
|
||||
StopGracePeriod?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
const { inspectMock, getServiceMock, createServiceMock, getRemoteDockerMock } =
|
||||
vi.hoisted(() => {
|
||||
const inspect = vi.fn<[], Promise<never>>();
|
||||
const getService = vi.fn(() => ({ inspect }));
|
||||
const createService = vi.fn<[MockCreateServiceOptions], Promise<void>>(
|
||||
async () => undefined,
|
||||
);
|
||||
const getRemoteDocker = vi.fn(async () => ({
|
||||
getService,
|
||||
createService,
|
||||
}));
|
||||
return {
|
||||
inspectMock: inspect,
|
||||
getServiceMock: getService,
|
||||
createServiceMock: createService,
|
||||
getRemoteDockerMock: getRemoteDocker,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@dokploy/server/utils/servers/remote-docker", () => ({
|
||||
getRemoteDocker: getRemoteDockerMock,
|
||||
}));
|
||||
|
||||
const createApplication = (
|
||||
overrides: Partial<ApplicationNested> = {},
|
||||
): ApplicationNested =>
|
||||
({
|
||||
appName: "test-app",
|
||||
buildType: "dockerfile",
|
||||
env: null,
|
||||
mounts: [],
|
||||
cpuLimit: null,
|
||||
memoryLimit: null,
|
||||
memoryReservation: null,
|
||||
cpuReservation: null,
|
||||
command: null,
|
||||
ports: [],
|
||||
sourceType: "docker",
|
||||
dockerImage: "example:latest",
|
||||
registry: null,
|
||||
environment: {
|
||||
project: { env: null },
|
||||
env: null,
|
||||
},
|
||||
replicas: 1,
|
||||
stopGracePeriodSwarm: 0n,
|
||||
serverId: "server-id",
|
||||
...overrides,
|
||||
}) as unknown as ApplicationNested;
|
||||
|
||||
describe("mechanizeDockerContainer", () => {
|
||||
beforeEach(() => {
|
||||
inspectMock.mockReset();
|
||||
inspectMock.mockRejectedValue(new Error("service not found"));
|
||||
getServiceMock.mockClear();
|
||||
createServiceMock.mockClear();
|
||||
getRemoteDockerMock.mockClear();
|
||||
getRemoteDockerMock.mockResolvedValue({
|
||||
getService: getServiceMock,
|
||||
createService: createServiceMock,
|
||||
});
|
||||
});
|
||||
|
||||
it("converts bigint stopGracePeriodSwarm to a number and keeps zero values", async () => {
|
||||
const application = createApplication({ stopGracePeriodSwarm: 0n });
|
||||
|
||||
await mechanizeDockerContainer(application);
|
||||
|
||||
expect(createServiceMock).toHaveBeenCalledTimes(1);
|
||||
const call = createServiceMock.mock.calls[0];
|
||||
if (!call) {
|
||||
throw new Error("createServiceMock should have been called once");
|
||||
}
|
||||
const [settings] = call;
|
||||
expect(settings.StopGracePeriod).toBe(0);
|
||||
expect(typeof settings.StopGracePeriod).toBe("number");
|
||||
});
|
||||
|
||||
it("omits StopGracePeriod when stopGracePeriodSwarm is null", async () => {
|
||||
const application = createApplication({ stopGracePeriodSwarm: null });
|
||||
|
||||
await mechanizeDockerContainer(application);
|
||||
|
||||
expect(createServiceMock).toHaveBeenCalledTimes(1);
|
||||
const call = createServiceMock.mock.calls[0];
|
||||
if (!call) {
|
||||
throw new Error("createServiceMock should have been called once");
|
||||
}
|
||||
const [settings] = call;
|
||||
expect(settings).not.toHaveProperty("StopGracePeriod");
|
||||
});
|
||||
});
|
||||
@@ -228,5 +228,58 @@ describe("helpers functions", () => {
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI",
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle JWT payload with newlines and whitespace by trimming them", () => {
|
||||
const iat = Math.floor(new Date("2025-01-01T00:00:00Z").getTime() / 1000);
|
||||
const expiry = iat + 3600;
|
||||
const payloadWithNewlines = `{
|
||||
"role": "anon",
|
||||
"iss": "supabase",
|
||||
"exp": ${expiry}
|
||||
}
|
||||
`;
|
||||
const jwt = processValue(
|
||||
"${jwt:secret:payload}",
|
||||
{
|
||||
secret: "mysecret",
|
||||
payload: payloadWithNewlines,
|
||||
},
|
||||
mockSchema,
|
||||
);
|
||||
expect(jwt).toMatch(jwtMatchExp);
|
||||
const parts = jwt.split(".") as JWTParts;
|
||||
jwtCheckHeader(parts[0]);
|
||||
const decodedPayload = jwtBase64Decode(parts[1]);
|
||||
expect(decodedPayload).toHaveProperty("role");
|
||||
expect(decodedPayload.role).toEqual("anon");
|
||||
expect(decodedPayload).toHaveProperty("iss");
|
||||
expect(decodedPayload.iss).toEqual("supabase");
|
||||
expect(decodedPayload).toHaveProperty("exp");
|
||||
expect(decodedPayload.exp).toEqual(expiry);
|
||||
});
|
||||
|
||||
it("should handle JWT payload with leading and trailing whitespace", () => {
|
||||
const iat = Math.floor(new Date("2025-01-01T00:00:00Z").getTime() / 1000);
|
||||
const expiry = iat + 3600;
|
||||
const payloadWithWhitespace = ` {"role": "service_role", "iss": "supabase", "exp": ${expiry}} `;
|
||||
const jwt = processValue(
|
||||
"${jwt:secret:payload}",
|
||||
{
|
||||
secret: "mysecret",
|
||||
payload: payloadWithWhitespace,
|
||||
},
|
||||
mockSchema,
|
||||
);
|
||||
expect(jwt).toMatch(jwtMatchExp);
|
||||
const parts = jwt.split(".") as JWTParts;
|
||||
jwtCheckHeader(parts[0]);
|
||||
const decodedPayload = jwtBase64Decode(parts[1]);
|
||||
expect(decodedPayload).toHaveProperty("role");
|
||||
expect(decodedPayload.role).toEqual("service_role");
|
||||
expect(decodedPayload).toHaveProperty("iss");
|
||||
expect(decodedPayload.iss).toEqual("supabase");
|
||||
expect(decodedPayload).toHaveProperty("exp");
|
||||
expect(decodedPayload.exp).toEqual(expiry);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { Domain } from "@dokploy/server";
|
||||
import type { Redirect } from "@dokploy/server";
|
||||
import type { ApplicationNested } from "@dokploy/server";
|
||||
import type { ApplicationNested, Domain, Redirect } from "@dokploy/server";
|
||||
import { createRouterConfig } from "@dokploy/server";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const baseApp: ApplicationNested = {
|
||||
railpackVersion: "0.2.2",
|
||||
rollbackActive: false,
|
||||
applicationId: "",
|
||||
previewLabels: [],
|
||||
herokuVersion: "",
|
||||
giteaRepository: "",
|
||||
giteaOwner: "",
|
||||
@@ -18,14 +18,17 @@ const baseApp: ApplicationNested = {
|
||||
appName: "",
|
||||
autoDeploy: true,
|
||||
enableSubmodules: false,
|
||||
previewRequireCollaboratorPermissions: false,
|
||||
serverId: "",
|
||||
branch: null,
|
||||
dockerBuildStage: "",
|
||||
registryUrl: "",
|
||||
watchPaths: [],
|
||||
buildArgs: null,
|
||||
buildSecrets: null,
|
||||
isPreviewDeploymentsActive: false,
|
||||
previewBuildArgs: null,
|
||||
previewBuildSecrets: null,
|
||||
triggerType: "push",
|
||||
previewCertificateType: "none",
|
||||
previewEnv: null,
|
||||
@@ -35,13 +38,22 @@ const baseApp: ApplicationNested = {
|
||||
previewLimit: 0,
|
||||
previewCustomCertResolver: null,
|
||||
previewWildcard: "",
|
||||
project: {
|
||||
environmentId: "",
|
||||
environment: {
|
||||
env: "",
|
||||
organizationId: "",
|
||||
environmentId: "",
|
||||
name: "",
|
||||
description: "",
|
||||
createdAt: "",
|
||||
description: "",
|
||||
projectId: "",
|
||||
project: {
|
||||
env: "",
|
||||
organizationId: "",
|
||||
name: "",
|
||||
description: "",
|
||||
createdAt: "",
|
||||
projectId: "",
|
||||
},
|
||||
},
|
||||
buildPath: "/",
|
||||
gitlabPathNamespace: "",
|
||||
@@ -84,7 +96,6 @@ const baseApp: ApplicationNested = {
|
||||
password: null,
|
||||
placementSwarm: null,
|
||||
ports: [],
|
||||
projectId: "",
|
||||
publishDirectory: null,
|
||||
isStaticSpa: null,
|
||||
redirects: [],
|
||||
@@ -102,6 +113,7 @@ const baseApp: ApplicationNested = {
|
||||
updateConfigSwarm: null,
|
||||
username: null,
|
||||
dockerContextPath: null,
|
||||
stopGracePeriodSwarm: null,
|
||||
};
|
||||
|
||||
const baseDomain: Domain = {
|
||||
|
||||
@@ -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 { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -19,6 +25,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -26,12 +33,6 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
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
|
||||
.object({
|
||||
@@ -176,26 +177,51 @@ const addSwarmSettings = z.object({
|
||||
modeSwarm: createStringToJSONSchema(ServiceModeSwarmSchema).nullable(),
|
||||
labelsSwarm: createStringToJSONSchema(LabelsSwarmSchema).nullable(),
|
||||
networkSwarm: createStringToJSONSchema(NetworkSwarmSchema).nullable(),
|
||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||
});
|
||||
|
||||
type AddSwarmSettings = z.infer<typeof addSwarmSettings>;
|
||||
|
||||
const hasStopGracePeriodSwarm = (
|
||||
value: unknown,
|
||||
): value is { stopGracePeriodSwarm: bigint | number | string | null } =>
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"stopGracePeriodSwarm" in value;
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
id: string;
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
}
|
||||
|
||||
export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||
const { data, refetch } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{
|
||||
enabled: !!applicationId,
|
||||
},
|
||||
);
|
||||
export const AddSwarmSettings = ({ id, type }: Props) => {
|
||||
const queryMap = {
|
||||
postgres: () =>
|
||||
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 }),
|
||||
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 } =
|
||||
api.application.update.useMutation();
|
||||
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, isError, error, isLoading } = mutationMap[type]
|
||||
? mutationMap[type]()
|
||||
: api.mongo.update.useMutation();
|
||||
|
||||
const form = useForm<AddSwarmSettings>({
|
||||
defaultValues: {
|
||||
@@ -207,12 +233,22 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||
modeSwarm: null,
|
||||
labelsSwarm: null,
|
||||
networkSwarm: null,
|
||||
stopGracePeriodSwarm: null,
|
||||
},
|
||||
resolver: zodResolver(addSwarmSettings),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const stopGracePeriodValue = hasStopGracePeriodSwarm(data)
|
||||
? data.stopGracePeriodSwarm
|
||||
: null;
|
||||
const normalizedStopGracePeriod =
|
||||
stopGracePeriodValue === null || stopGracePeriodValue === undefined
|
||||
? null
|
||||
: typeof stopGracePeriodValue === "bigint"
|
||||
? stopGracePeriodValue
|
||||
: BigInt(stopGracePeriodValue);
|
||||
form.reset({
|
||||
healthCheckSwarm: data.healthCheckSwarm
|
||||
? JSON.stringify(data.healthCheckSwarm, null, 2)
|
||||
@@ -238,13 +274,19 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||
networkSwarm: data.networkSwarm
|
||||
? JSON.stringify(data.networkSwarm, null, 2)
|
||||
: null,
|
||||
stopGracePeriodSwarm: normalizedStopGracePeriod,
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
const onSubmit = async (data: AddSwarmSettings) => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
applicationId: id || "",
|
||||
postgresId: id || "",
|
||||
redisId: id || "",
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
healthCheckSwarm: data.healthCheckSwarm,
|
||||
restartPolicySwarm: data.restartPolicySwarm,
|
||||
placementSwarm: data.placementSwarm,
|
||||
@@ -253,6 +295,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||
modeSwarm: data.modeSwarm,
|
||||
labelsSwarm: data.labelsSwarm,
|
||||
networkSwarm: data.networkSwarm,
|
||||
stopGracePeriodSwarm: data.stopGracePeriodSwarm ?? null,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Swarm settings updated");
|
||||
@@ -270,18 +313,18 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||
Swarm Settings
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-5xl p-0">
|
||||
<DialogHeader className="p-6">
|
||||
<DialogContent className="sm:max-w-5xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Swarm Settings</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update certain settings using a json object.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<div className="px-4">
|
||||
<div>
|
||||
<AlertBlock type="info">
|
||||
Changing settings such as placements may cause the logs/monitoring
|
||||
to be unavailable.
|
||||
Changing settings such as placements may cause the logs/monitoring,
|
||||
backups and other features to be unavailable.
|
||||
</AlertBlock>
|
||||
</div>
|
||||
|
||||
@@ -289,13 +332,13 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||
<form
|
||||
id="hook-form-add-permissions"
|
||||
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
|
||||
control={form.control}
|
||||
name="healthCheckSwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
|
||||
<FormItem className="relative ">
|
||||
<FormLabel>Health Check</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
@@ -330,9 +373,9 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||
language="json"
|
||||
placeholder={`{
|
||||
"Test" : ["CMD-SHELL", "curl -f http://localhost:3000/health"],
|
||||
"Interval" : 10000,
|
||||
"Timeout" : 10000,
|
||||
"StartPeriod" : 10000,
|
||||
"Interval" : 10000000000,
|
||||
"Timeout" : 10000000000,
|
||||
"StartPeriod" : 10000000000,
|
||||
"Retries" : 10
|
||||
}`}
|
||||
className="h-[12rem] font-mono"
|
||||
@@ -351,7 +394,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="restartPolicySwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
|
||||
<FormItem className="relative ">
|
||||
<FormLabel>Restart Policy</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
@@ -385,9 +428,9 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||
language="json"
|
||||
placeholder={`{
|
||||
"Condition" : "on-failure",
|
||||
"Delay" : 10000,
|
||||
"Delay" : 10000000000,
|
||||
"MaxAttempts" : 10,
|
||||
"Window" : 10000
|
||||
"Window" : 10000000000
|
||||
} `}
|
||||
className="h-[12rem] font-mono"
|
||||
{...field}
|
||||
@@ -405,7 +448,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="placementSwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
|
||||
<FormItem className="relative ">
|
||||
<FormLabel>Placement</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
@@ -471,7 +514,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="updateConfigSwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
|
||||
<FormItem className="relative ">
|
||||
<FormLabel>Update Config</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
@@ -507,9 +550,9 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||
language="json"
|
||||
placeholder={`{
|
||||
"Parallelism" : 1,
|
||||
"Delay" : 10000,
|
||||
"Delay" : 10000000000,
|
||||
"FailureAction" : "continue",
|
||||
"Monitor" : 10000,
|
||||
"Monitor" : 10000000000,
|
||||
"MaxFailureRatio" : 10,
|
||||
"Order" : "start-first"
|
||||
}`}
|
||||
@@ -529,7 +572,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="rollbackConfigSwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
|
||||
<FormItem className="relative ">
|
||||
<FormLabel>Rollback Config</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
@@ -565,9 +608,9 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||
language="json"
|
||||
placeholder={`{
|
||||
"Parallelism" : 1,
|
||||
"Delay" : 10000,
|
||||
"Delay" : 10000000000,
|
||||
"FailureAction" : "continue",
|
||||
"Monitor" : 10000,
|
||||
"Monitor" : 10000000000,
|
||||
"MaxFailureRatio" : 10,
|
||||
"Order" : "start-first"
|
||||
}`}
|
||||
@@ -587,7 +630,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="modeSwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
|
||||
<FormItem className="relative ">
|
||||
<FormLabel>Mode</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
@@ -650,7 +693,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="networkSwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
|
||||
<FormItem className="relative ">
|
||||
<FormLabel>Network</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
@@ -709,7 +752,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="labelsSwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
|
||||
<FormItem className="relative ">
|
||||
<FormLabel>Labels</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
@@ -752,8 +795,58 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<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 ">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="stopGracePeriodSwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
|
||||
<FormLabel>Stop Grace Period (nanoseconds)</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Duration in nanoseconds
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormDescription>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="w-full z-[999]"
|
||||
align="start"
|
||||
side="bottom"
|
||||
>
|
||||
<code>
|
||||
<pre>
|
||||
{`Enter duration in nanoseconds:
|
||||
• 30000000000 - 30 seconds
|
||||
• 120000000000 - 2 minutes
|
||||
• 3600000000000 - 1 hour
|
||||
• 0 - no grace period`}
|
||||
</pre>
|
||||
</code>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="30000000000"
|
||||
className="font-mono"
|
||||
{...field}
|
||||
value={field?.value?.toString() || ""}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target.value ? BigInt(e.target.value) : null,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2 m-0 sticky bottom-0 right-0 bg-muted border">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
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 { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -26,43 +33,57 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
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";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
id: string;
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
}
|
||||
|
||||
const AddRedirectchema = z.object({
|
||||
replicas: z.number().min(1, "Replicas must be at least 1"),
|
||||
registryId: z.string(),
|
||||
registryId: z.string().optional(),
|
||||
});
|
||||
|
||||
type AddCommand = z.infer<typeof AddRedirectchema>;
|
||||
|
||||
export const ShowClusterSettings = ({ applicationId }: Props) => {
|
||||
const { data } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
|
||||
export const ShowClusterSettings = ({ id, type }: Props) => {
|
||||
const queryMap = {
|
||||
postgres: () =>
|
||||
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 }),
|
||||
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 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>({
|
||||
defaultValues: {
|
||||
registryId: data?.registryId || "",
|
||||
...(type === "application" && data && "registryId" in data
|
||||
? {
|
||||
registryId: data?.registryId || "",
|
||||
}
|
||||
: {}),
|
||||
replicas: data?.replicas || 1,
|
||||
},
|
||||
resolver: zodResolver(AddRedirectchema),
|
||||
@@ -71,7 +92,11 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
|
||||
useEffect(() => {
|
||||
if (data?.command) {
|
||||
form.reset({
|
||||
registryId: data?.registryId || "",
|
||||
...(type === "application" && data && "registryId" in data
|
||||
? {
|
||||
registryId: data?.registryId || "",
|
||||
}
|
||||
: {}),
|
||||
replicas: data?.replicas || 1,
|
||||
});
|
||||
}
|
||||
@@ -79,18 +104,25 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
|
||||
|
||||
const onSubmit = async (data: AddCommand) => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
registryId:
|
||||
data?.registryId === "none" || !data?.registryId
|
||||
? null
|
||||
: data?.registryId,
|
||||
applicationId: id || "",
|
||||
postgresId: id || "",
|
||||
redisId: id || "",
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
...(type === "application"
|
||||
? {
|
||||
registryId:
|
||||
data?.registryId === "none" || !data?.registryId
|
||||
? null
|
||||
: data?.registryId,
|
||||
}
|
||||
: {}),
|
||||
replicas: data?.replicas,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Command Updated");
|
||||
await utils.application.one.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating the command");
|
||||
@@ -103,10 +135,10 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
|
||||
<div>
|
||||
<CardTitle className="text-xl">Cluster Settings</CardTitle>
|
||||
<CardDescription>
|
||||
Add the registry and the replicas of the application
|
||||
Modify swarm settings for the service.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<AddSwarmSettings applicationId={applicationId} />
|
||||
<AddSwarmSettings id={id} type={type} />
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<AlertBlock type="info">
|
||||
@@ -144,58 +176,62 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{registries && registries?.length === 0 ? (
|
||||
<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>
|
||||
) : (
|
||||
{type === "application" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="registryId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Select a registry</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
{registries && registries?.length === 0 ? (
|
||||
<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
|
||||
control={form.control}
|
||||
name="registryId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Select a registry</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<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 {
|
||||
Card,
|
||||
@@ -16,11 +21,7 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
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 {
|
||||
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 { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -27,12 +33,6 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
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({
|
||||
base64: z.string(),
|
||||
@@ -185,7 +185,7 @@ export const ShowImport = ({ composeId }: Props) => {
|
||||
</Button>
|
||||
</div>
|
||||
<Dialog open={showModal} onOpenChange={setShowModal}>
|
||||
<DialogContent className="max-h-[80vh] max-w-[50vw] overflow-y-auto">
|
||||
<DialogContent className="max-w-[50vw]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-bold">
|
||||
Template Information
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm, useWatch } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -26,12 +32,6 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
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({
|
||||
publishedPort: z.number().int().min(1).max(65535),
|
||||
@@ -80,6 +80,11 @@ export const HandlePorts = ({
|
||||
resolver: zodResolver(AddPortSchema),
|
||||
});
|
||||
|
||||
const publishMode = useWatch({
|
||||
control: form.control,
|
||||
name: "publishMode",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
publishedPort: data?.publishedPort ?? 0,
|
||||
@@ -124,7 +129,7 @@ export const HandlePorts = ({
|
||||
<Button>{children}</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Ports</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -253,6 +258,16 @@ export const HandlePorts = ({
|
||||
</div>
|
||||
</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>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Rss, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -9,9 +11,8 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { Rss, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { HandlePorts } from "./handle-ports";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
@@ -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 { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -30,12 +36,6 @@ import {
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
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({
|
||||
regex: z.string().min(1, "Regex required"),
|
||||
@@ -179,7 +179,7 @@ export const HandleRedirect = ({
|
||||
<Button>{children}</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Redirects</DialogTitle>
|
||||
<DialogDescription>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Split, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -8,8 +10,6 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { Split, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { HandleRedirect } from "./handle-redirect";
|
||||
|
||||
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 { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -19,12 +25,6 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
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({
|
||||
username: z.string().min(1, "Username is required"),
|
||||
@@ -114,7 +114,7 @@ export const HandleSecurity = ({
|
||||
<Button>{children}</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Security</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -151,7 +151,7 @@ export const HandleSecurity = ({
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="test" {...field} />
|
||||
<Input placeholder="test" type="password" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { LockKeyhole, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -7,9 +10,9 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { LockKeyhole, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { HandleSecurity } from "./handle-security";
|
||||
|
||||
interface Props {
|
||||
@@ -58,19 +61,18 @@ export const ShowSecurity = ({ applicationId }: Props) => {
|
||||
<div className="flex flex-col gap-6 ">
|
||||
{data?.security.map((security) => (
|
||||
<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="grid grid-cols-1 sm:grid-cols-2 flex-col gap-4 sm:gap-8">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium">Username</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{security.username}
|
||||
</span>
|
||||
<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 md:grid-cols-2 flex-col gap-4 md:gap-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Username</Label>
|
||||
<Input disabled value={security.username} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium">Password</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{security.password}
|
||||
</span>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Password</Label>
|
||||
<ToggleVisibilityInput
|
||||
value={security.password}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<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 { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -23,12 +29,6 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
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({
|
||||
memoryReservation: z.string().optional(),
|
||||
@@ -150,7 +150,10 @@ export const ShowResources = ({ id, type }: Props) => {
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="flex items-center gap-2"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<FormLabel>Memory Limit</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
@@ -182,7 +185,10 @@ export const ShowResources = ({ id, type }: Props) => {
|
||||
name="memoryReservation"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="flex items-center gap-2"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<FormLabel>Memory Reservation</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
@@ -215,7 +221,10 @@ export const ShowResources = ({ id, type }: Props) => {
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="flex items-center gap-2"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<FormLabel>CPU Limit</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
@@ -249,7 +258,10 @@ export const ShowResources = ({ id, type }: Props) => {
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="flex items-center gap-2"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<FormLabel>CPU Reservation</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { File, Loader2 } from "lucide-react";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import {
|
||||
Card,
|
||||
@@ -7,8 +8,8 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { File, Loader2 } from "lucide-react";
|
||||
import { UpdateTraefikConfig } from "./update-traefik-config";
|
||||
|
||||
interface Props {
|
||||
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 { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -19,12 +25,6 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
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({
|
||||
traefikConfig: z.string(),
|
||||
@@ -38,11 +38,11 @@ interface Props {
|
||||
|
||||
export const validateAndFormatYAML = (yamlText: string) => {
|
||||
try {
|
||||
const obj = jsyaml.load(yamlText);
|
||||
const formattedYaml = jsyaml.dump(obj, { indent: 4 });
|
||||
const obj = parse(yamlText);
|
||||
const formattedYaml = stringify(obj, { indent: 4 });
|
||||
return { valid: true, formattedYaml, error: null };
|
||||
} catch (error) {
|
||||
if (error instanceof jsyaml.YAMLException) {
|
||||
if (error instanceof YAMLParseError) {
|
||||
return {
|
||||
valid: false,
|
||||
formattedYaml: yamlText,
|
||||
@@ -89,7 +89,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||
if (!valid) {
|
||||
form.setError("traefikConfig", {
|
||||
type: "manual",
|
||||
message: error || "Invalid YAML",
|
||||
message: (error as string) || "Invalid YAML",
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -122,7 +122,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||
<DialogTrigger asChild>
|
||||
<Button isLoading={isLoading}>Modify</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
|
||||
<DialogContent className="sm:max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update traefik config</DialogTitle>
|
||||
<DialogDescription>Update the traefik config</DialogDescription>
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
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 { Button } from "@/components/ui/button";
|
||||
@@ -22,13 +29,7 @@ import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { cn } from "@/lib/utils";
|
||||
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 {
|
||||
serviceId: string;
|
||||
serviceType:
|
||||
@@ -151,7 +152,7 @@ export const AddVolumes = ({
|
||||
<DialogTrigger className="" asChild>
|
||||
<Button>{children}</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-3xl">
|
||||
<DialogContent className="sm:max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Volumes / Mounts</DialogTitle>
|
||||
</DialogHeader>
|
||||
@@ -317,7 +318,7 @@ export const AddVolumes = ({
|
||||
control={form.control}
|
||||
name="content"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormItem className="max-w-full max-w-[45rem]">
|
||||
<FormLabel>Content</FormLabel>
|
||||
<FormControl>
|
||||
<FormControl>
|
||||
@@ -326,7 +327,7 @@ export const AddVolumes = ({
|
||||
placeholder={`NODE_ENV=production
|
||||
PORT=3000
|
||||
`}
|
||||
className="h-96 font-mono"
|
||||
className="h-96 font-mono "
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Package, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -9,11 +11,10 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { Package, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import type { ServiceType } from "../show-resources";
|
||||
import { AddVolumes } from "./add-volumes";
|
||||
import { UpdateVolume } from "./update-volume";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
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"
|
||||
>
|
||||
{/* <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">
|
||||
<span className="font-medium">Mount Type</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
@@ -112,21 +113,21 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{mount.type === "file" ? (
|
||||
{mount.type === "file" && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium">File Path</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{mount.filePath}
|
||||
</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 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 className="flex flex-row gap-1">
|
||||
<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 { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -20,12 +26,6 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
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({
|
||||
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" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-3xl">
|
||||
<DialogContent className="sm:max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update</DialogTitle>
|
||||
<DialogDescription>Update the mount</DialogDescription>
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Cog } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -15,12 +21,6 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
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 {
|
||||
dockerfile = "dockerfile",
|
||||
@@ -65,6 +65,7 @@ const mySchema = z.discriminatedUnion("buildType", [
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal(BuildType.railpack),
|
||||
railpackVersion: z.string().nullable().default("0.2.2"),
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal(BuildType.static),
|
||||
@@ -86,6 +87,7 @@ interface ApplicationData {
|
||||
herokuVersion?: string | null;
|
||||
publishDirectory?: string | null;
|
||||
isStaticSpa?: boolean | null;
|
||||
railpackVersion?: string | null | undefined;
|
||||
}
|
||||
|
||||
function isValidBuildType(value: string): value is BuildType {
|
||||
@@ -123,6 +125,7 @@ const resetData = (data: ApplicationData): AddTemplate => {
|
||||
case BuildType.railpack:
|
||||
return {
|
||||
buildType: BuildType.railpack,
|
||||
railpackVersion: data.railpackVersion || null,
|
||||
};
|
||||
default: {
|
||||
const buildType = data.buildType as BuildType;
|
||||
@@ -181,6 +184,10 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||
: null,
|
||||
isStaticSpa:
|
||||
data.buildType === BuildType.static ? data.isStaticSpa : null,
|
||||
railpackVersion:
|
||||
data.buildType === BuildType.railpack
|
||||
? data.railpackVersion || "0.2.2"
|
||||
: null,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Build type saved");
|
||||
@@ -395,6 +402,25 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{buildType === BuildType.railpack && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="railpackVersion"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Railpack Version</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Railpack Version"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div className="flex w-full justify-end">
|
||||
<Button isLoading={isLoading} type="submit">
|
||||
Save
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Paintbrush } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -11,8 +13,6 @@ import {
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { Paintbrush } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -10,8 +12,6 @@ import {
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { api } from "@/utils/api";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import copy from "copy-to-clipboard";
|
||||
import { Check, Copy, Loader2 } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -7,8 +11,6 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { TerminalLine } from "../../docker/logs/terminal-line";
|
||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||
|
||||
@@ -29,9 +31,10 @@ export const ShowDeployment = ({
|
||||
const [data, setData] = useState("");
|
||||
const [showExtraLogs, setShowExtraLogs] = useState(false);
|
||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const [autoScroll, setAutoScroll] = useState(true);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (autoScroll && scrollRef.current) {
|
||||
@@ -106,6 +109,20 @@ export const ShowDeployment = ({
|
||||
}
|
||||
}, [filteredLogs, autoScroll]);
|
||||
|
||||
const handleCopy = () => {
|
||||
const logContent = filteredLogs
|
||||
.map(({ timestamp, message }: LogLine) =>
|
||||
`${timestamp?.toISOString() || ""} ${message}`.trim(),
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
const success = copy(logContent);
|
||||
if (success) {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const optionalErrors = parseLogs(errorMessage || "");
|
||||
|
||||
return (
|
||||
@@ -124,17 +141,31 @@ export const ShowDeployment = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
||||
<DialogContent className={"sm:max-w-5xl"}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Deployment</DialogTitle>
|
||||
<DialogDescription className="flex items-center gap-2">
|
||||
<span>
|
||||
<span className="flex items-center gap-2">
|
||||
See all the details of this deployment |{" "}
|
||||
<Badge variant="blank" className="text-xs">
|
||||
{filteredLogs.length} lines
|
||||
</Badge>
|
||||
</span>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7"
|
||||
onClick={handleCopy}
|
||||
disabled={filteredLogs.length === 0}
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{serverId && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
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 { useState } from "react";
|
||||
import { ShowDeployment } from "../deployments/show-deployment";
|
||||
import { ShowDeployments } from "./show-deployments";
|
||||
|
||||
@@ -50,7 +49,7 @@ export const ShowDeploymentsModal = ({
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl p-0">
|
||||
<DialogContent className="sm:max-w-5xl p-0">
|
||||
<ShowDeployments
|
||||
id={id}
|
||||
type={type}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
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 { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -9,15 +14,11 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { type RouterOutputs, api } from "@/utils/api";
|
||||
import { Clock, Loader2, RocketIcon, Settings, RefreshCcw } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { api, type RouterOutputs } from "@/utils/api";
|
||||
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
|
||||
import { CancelQueues } from "./cancel-queues";
|
||||
import { RefreshToken } from "./refresh-token";
|
||||
import { ShowDeployment } from "./show-deployment";
|
||||
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@@ -61,12 +62,48 @@ export const ShowDeployments = ({
|
||||
},
|
||||
);
|
||||
|
||||
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("");
|
||||
|
||||
// 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(() => {
|
||||
setUrl(document.location.origin);
|
||||
}, []);
|
||||
@@ -77,7 +114,7 @@ export const ShowDeployments = ({
|
||||
<div className="flex flex-col gap-2">
|
||||
<CardTitle className="text-xl">Deployments</CardTitle>
|
||||
<CardDescription>
|
||||
See all the 10 last deployments for this {type}
|
||||
See the last 10 deployments for this {type}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
@@ -94,6 +131,54 @@ export const ShowDeployments = ({
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{stuckDeployment && (type === "application" || type === "compose") && (
|
||||
<AlertBlock
|
||||
type="warning"
|
||||
className="flex-col items-start w-full p-4"
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div>
|
||||
<div className="font-medium text-sm mb-1">
|
||||
Build appears to be stuck
|
||||
</div>
|
||||
<p className="text-sm">
|
||||
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>
|
||||
@@ -104,7 +189,9 @@ export const ShowDeployments = ({
|
||||
<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}`}
|
||||
{`${url}/api/deploy${
|
||||
type === "compose" ? "/compose" : ""
|
||||
}/${refreshToken}`}
|
||||
</span>
|
||||
{(type === "application" || type === "compose") && (
|
||||
<RefreshToken id={id} type={type} />
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
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 {
|
||||
@@ -8,8 +10,6 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Copy, HelpCircle, Server } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
domain: {
|
||||
@@ -33,7 +33,7 @@ export const DnsHelperModal = ({ domain, serverIp }: Props) => {
|
||||
<HelpCircle className="size-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Server className="size-5" />
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import z from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -34,14 +41,6 @@ import {
|
||||
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 { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import z from "zod";
|
||||
|
||||
export type CacheType = "fetch" | "cache";
|
||||
|
||||
@@ -123,6 +122,7 @@ interface Props {
|
||||
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(
|
||||
@@ -292,7 +292,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
<DialogTrigger className="" asChild>
|
||||
{children}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Domain</DialogTitle>
|
||||
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||
@@ -325,46 +325,126 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>Service Name</FormLabel>
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value || ""}
|
||||
>
|
||||
{isManualInput ? (
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a service name" />
|
||||
</SelectTrigger>
|
||||
<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}
|
||||
<SelectContent>
|
||||
{services?.map((service, index) => (
|
||||
<SelectItem
|
||||
value={service}
|
||||
key={`${service}-${index}`}
|
||||
>
|
||||
{service}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value="none" disabled>
|
||||
Empty
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value="none" disabled>
|
||||
Empty
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</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"
|
||||
isLoading={isLoadingServices}
|
||||
onClick={() => {
|
||||
if (cacheType === "fetch") {
|
||||
refetchServices();
|
||||
} else {
|
||||
setCacheType("fetch");
|
||||
setIsManualInput(!isManualInput);
|
||||
if (!isManualInput) {
|
||||
field.onChange("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RefreshCw className="size-4 text-muted-foreground" />
|
||||
{isManualInput ? (
|
||||
<RefreshCw className="size-4 text-muted-foreground" />
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Manual
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
@@ -373,40 +453,9 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
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
|
||||
{isManualInput
|
||||
? "Switch to service selection"
|
||||
: "Enter service name manually"}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
import {
|
||||
CheckCircle2,
|
||||
ExternalLink,
|
||||
GlobeIcon,
|
||||
InfoIcon,
|
||||
Loader2,
|
||||
PenBoxIcon,
|
||||
RefreshCw,
|
||||
Server,
|
||||
Trash2,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -15,21 +30,6 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
CheckCircle2,
|
||||
ExternalLink,
|
||||
GlobeIcon,
|
||||
InfoIcon,
|
||||
Loader2,
|
||||
PenBoxIcon,
|
||||
RefreshCw,
|
||||
Server,
|
||||
Trash2,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DnsHelperModal } from "./dns-helper-modal";
|
||||
import { AddDomain } from "./handle-domain";
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||
import { type CSSProperties, useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -16,12 +22,6 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Toggle } from "@/components/ui/toggle";
|
||||
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";
|
||||
|
||||
const addEnvironmentSchema = z.object({
|
||||
@@ -108,6 +108,21 @@ export const ShowEnvironment = ({ id, type }: Props) => {
|
||||
});
|
||||
};
|
||||
|
||||
// Add keyboard shortcut for Ctrl+S/Cmd+S
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
|
||||
e.preventDefault();
|
||||
form.handleSubmit(onSubmit)();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [form, onSubmit, isLoading]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-5 ">
|
||||
<Card className="bg-background">
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
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 { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
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({
|
||||
env: z.string(),
|
||||
buildArgs: z.string(),
|
||||
buildSecrets: z.string(),
|
||||
});
|
||||
|
||||
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
|
||||
@@ -37,6 +38,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
defaultValues: {
|
||||
env: "",
|
||||
buildArgs: "",
|
||||
buildSecrets: "",
|
||||
},
|
||||
resolver: zodResolver(addEnvironmentSchema),
|
||||
});
|
||||
@@ -44,15 +46,18 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
// Watch form values
|
||||
const currentEnv = form.watch("env");
|
||||
const currentBuildArgs = form.watch("buildArgs");
|
||||
const currentBuildSecrets = form.watch("buildSecrets");
|
||||
const hasChanges =
|
||||
currentEnv !== (data?.env || "") ||
|
||||
currentBuildArgs !== (data?.buildArgs || "");
|
||||
currentBuildArgs !== (data?.buildArgs || "") ||
|
||||
currentBuildSecrets !== (data?.buildSecrets || "");
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
env: data.env || "",
|
||||
buildArgs: data.buildArgs || "",
|
||||
buildSecrets: data.buildSecrets || "",
|
||||
});
|
||||
}
|
||||
}, [data, form]);
|
||||
@@ -61,6 +66,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
mutateAsync({
|
||||
env: formData.env,
|
||||
buildArgs: formData.buildArgs,
|
||||
buildSecrets: formData.buildSecrets,
|
||||
applicationId,
|
||||
})
|
||||
.then(async () => {
|
||||
@@ -76,9 +82,25 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
form.reset({
|
||||
env: data?.env || "",
|
||||
buildArgs: data?.buildArgs || "",
|
||||
buildSecrets: data?.buildSecrets || "",
|
||||
});
|
||||
};
|
||||
|
||||
// Add keyboard shortcut for Ctrl+S/Cmd+S
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
|
||||
e.preventDefault();
|
||||
form.handleSubmit(onSubmit)();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [form, onSubmit, isLoading]);
|
||||
|
||||
return (
|
||||
<Card className="bg-background px-6 pb-6">
|
||||
<Form {...form}>
|
||||
@@ -104,13 +126,36 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
{data?.buildType === "dockerfile" && (
|
||||
<Secrets
|
||||
name="buildArgs"
|
||||
title="Build-time Variables"
|
||||
title="Build-time Arguments"
|
||||
description={
|
||||
<span>
|
||||
Available only at build-time. See documentation
|
||||
Arguments are available only at build-time. See
|
||||
documentation
|
||||
<a
|
||||
className="text-primary"
|
||||
href="https://docs.docker.com/build/guide/build-args/"
|
||||
href="https://docs.docker.com/build/building/variables/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
}
|
||||
placeholder="NPM_TOKEN=xyz"
|
||||
/>
|
||||
)}
|
||||
{data?.buildType === "dockerfile" && (
|
||||
<Secrets
|
||||
name="buildSecrets"
|
||||
title="Build-time Secrets"
|
||||
description={
|
||||
<span>
|
||||
Secrets are specially designed for sensitive information and
|
||||
are only available at build-time. See documentation
|
||||
<a
|
||||
className="text-primary"
|
||||
href="https://docs.docker.com/build/building/secrets/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
@@ -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 { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -40,13 +47,6 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
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({
|
||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||
@@ -150,7 +150,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
enableSubmodules: data.enableSubmodules || false,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Service Provided Saved");
|
||||
toast.success("Service Provider Saved");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@@ -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 {
|
||||
Form,
|
||||
@@ -9,11 +14,6 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
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({
|
||||
dockerImage: z.string().min(1, {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dropzone } from "@/components/ui/dropzone";
|
||||
import {
|
||||
@@ -11,11 +16,6 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
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 {
|
||||
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 {
|
||||
Form,
|
||||
@@ -25,17 +35,6 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
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({
|
||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||
@@ -60,7 +59,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { mutateAsync, isLoading } =
|
||||
api.application.saveGitProdiver.useMutation();
|
||||
api.application.saveGitProvider.useMutation();
|
||||
|
||||
const form = useForm<GitProvider>({
|
||||
defaultValues: {
|
||||
|
||||
@@ -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 { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -40,13 +47,6 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
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 {
|
||||
name: string;
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -39,13 +46,6 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
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({
|
||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||
@@ -149,7 +149,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
enableSubmodules: data.enableSubmodules,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Service Provided Saved");
|
||||
toast.success("Service Provider Saved");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@@ -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 { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -40,13 +47,6 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
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({
|
||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||
@@ -96,6 +96,16 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
const repository = form.watch("repository");
|
||||
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 {
|
||||
data: repositories,
|
||||
isLoading: isLoadingRepositories,
|
||||
@@ -157,7 +167,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
enableSubmodules: data.enableSubmodules,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Service Provided Saved");
|
||||
toast.success("Service Provider Saved");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -224,7 +234,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
<FormLabel>Repository</FormLabel>
|
||||
{field.value.owner && field.value.repo && (
|
||||
<Link
|
||||
href={`https://gitlab.com/${field.value.owner}/${field.value.repo}`}
|
||||
href={`${gitlabUrl}/${field.value.owner}/${field.value.repo}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||
|
||||
@@ -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 { SaveGitProvider } from "@/components/dashboard/application/general/generic/save-git-provider";
|
||||
import { SaveGiteaProvider } from "@/components/dashboard/application/general/generic/save-gitea-provider";
|
||||
@@ -5,18 +9,14 @@ import { SaveGithubProvider } from "@/components/dashboard/application/general/g
|
||||
import {
|
||||
BitbucketIcon,
|
||||
DockerIcon,
|
||||
GitIcon,
|
||||
GiteaIcon,
|
||||
GithubIcon,
|
||||
GitIcon,
|
||||
GitlabIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { api } from "@/utils/api";
|
||||
import { GitBranch, Loader2, UploadCloud } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { SaveBitbucketProvider } from "./save-bitbucket-provider";
|
||||
import { SaveDragNDrop } from "./save-drag-n-drop";
|
||||
import { SaveGitlabProvider } from "./save-gitlab-provider";
|
||||
@@ -153,8 +153,8 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
||||
setSab(e as TabState);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
|
||||
<div className="flex flex-row items-center justify-between w-full overflow-auto">
|
||||
<TabsList className="flex gap-4 justify-start bg-transparent">
|
||||
<TabsTrigger
|
||||
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"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { AlertCircle, GitBranch, Unlink } from "lucide-react";
|
||||
import {
|
||||
BitbucketIcon,
|
||||
GitIcon,
|
||||
GiteaIcon,
|
||||
GithubIcon,
|
||||
GitIcon,
|
||||
GitlabIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
@@ -10,7 +11,6 @@ import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import type { RouterOutputs } from "@/utils/api";
|
||||
import { AlertCircle, GitBranch, Unlink } from "lucide-react";
|
||||
|
||||
interface Props {
|
||||
service:
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import {
|
||||
Ban,
|
||||
CheckCircle2,
|
||||
Hammer,
|
||||
RefreshCcw,
|
||||
Rocket,
|
||||
Terminal,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "sonner";
|
||||
import { ShowBuildChooseForm } from "@/components/dashboard/application/build/show";
|
||||
import { ShowProviderForm } from "@/components/dashboard/application/general/generic/show";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
@@ -11,18 +22,8 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
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";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
@@ -68,7 +69,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
toast.success("Application deployed successfully");
|
||||
refetch();
|
||||
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(() => {
|
||||
|
||||
@@ -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 {
|
||||
Card,
|
||||
@@ -18,9 +21,6 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect, useState } from "react";
|
||||
export const DockerLogs = dynamic(
|
||||
() =>
|
||||
import("@/components/dashboard/docker/logs/docker-logs-id").then(
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Dices } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import type z from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -33,15 +39,8 @@ import {
|
||||
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 type z from "zod";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
type Domain = z.infer<typeof domain>;
|
||||
|
||||
@@ -138,7 +137,7 @@ export const AddPreviewDomain = ({
|
||||
<DialogTrigger className="" asChild>
|
||||
{children}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Domain</DialogTitle>
|
||||
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
import {
|
||||
ExternalLink,
|
||||
FileText,
|
||||
GitPullRequest,
|
||||
Loader2,
|
||||
PenSquare,
|
||||
RocketIcon,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
@@ -13,16 +23,6 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
ExternalLink,
|
||||
FileText,
|
||||
GitPullRequest,
|
||||
Loader2,
|
||||
PenSquare,
|
||||
RocketIcon,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
|
||||
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
|
||||
import { AddPreviewDomain } from "./add-preview-domain";
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { HelpCircle, Plus, Settings2, X } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -27,25 +34,28 @@ import {
|
||||
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 { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Settings2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
env: z.string(),
|
||||
buildArgs: z.string(),
|
||||
buildSecrets: z.string(),
|
||||
wildcardDomain: z.string(),
|
||||
port: z.number(),
|
||||
previewLimit: z.number(),
|
||||
previewLabels: z.array(z.string()).optional(),
|
||||
previewHttps: z.boolean(),
|
||||
previewPath: z.string(),
|
||||
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]),
|
||||
previewCustomCertResolver: z.string().optional(),
|
||||
previewRequireCollaboratorPermissions: z.boolean(),
|
||||
})
|
||||
.superRefine((input, ctx) => {
|
||||
if (
|
||||
@@ -80,9 +90,11 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
wildcardDomain: "*.traefik.me",
|
||||
port: 3000,
|
||||
previewLimit: 3,
|
||||
previewLabels: [],
|
||||
previewHttps: false,
|
||||
previewPath: "/",
|
||||
previewCertificateType: "none",
|
||||
previewRequireCollaboratorPermissions: true,
|
||||
},
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
@@ -98,13 +110,17 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
form.reset({
|
||||
env: data.previewEnv || "",
|
||||
buildArgs: data.previewBuildArgs || "",
|
||||
buildSecrets: data.previewBuildSecrets || "",
|
||||
wildcardDomain: data.previewWildcard || "*.traefik.me",
|
||||
port: data.previewPort || 3000,
|
||||
previewLabels: data.previewLabels || [],
|
||||
previewLimit: data.previewLimit || 3,
|
||||
previewHttps: data.previewHttps || false,
|
||||
previewPath: data.previewPath || "/",
|
||||
previewCertificateType: data.previewCertificateType || "none",
|
||||
previewCustomCertResolver: data.previewCustomCertResolver || "",
|
||||
previewRequireCollaboratorPermissions:
|
||||
data.previewRequireCollaboratorPermissions || true,
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
@@ -113,14 +129,18 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
updateApplication({
|
||||
previewEnv: formData.env,
|
||||
previewBuildArgs: formData.buildArgs,
|
||||
previewBuildSecrets: formData.buildSecrets,
|
||||
previewWildcard: formData.wildcardDomain,
|
||||
previewPort: formData.port,
|
||||
previewLabels: formData.previewLabels,
|
||||
applicationId,
|
||||
previewLimit: formData.previewLimit,
|
||||
previewHttps: formData.previewHttps,
|
||||
previewPath: formData.previewPath,
|
||||
previewCertificateType: formData.previewCertificateType,
|
||||
previewCustomCertResolver: formData.previewCustomCertResolver,
|
||||
previewRequireCollaboratorPermissions:
|
||||
formData.previewRequireCollaboratorPermissions,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Preview Deployments settings updated");
|
||||
@@ -138,7 +158,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
Configure
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl w-full">
|
||||
<DialogContent className="sm:max-w-5xl w-full">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Preview Deployment Settings</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -194,6 +214,90 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="previewLabels"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>Preview Labels</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Add a labels that will trigger a preview
|
||||
deployment for a pull request. If no labels
|
||||
are specified, all pull requests will trigger
|
||||
a preview deployment.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{field.value?.map((label, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant="secondary"
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
{label}
|
||||
<X
|
||||
className="size-3 cursor-pointer hover:text-destructive"
|
||||
onClick={() => {
|
||||
const newLabels = [...(field.value || [])];
|
||||
newLabels.splice(index, 1);
|
||||
field.onChange(newLabels);
|
||||
}}
|
||||
/>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter a label (e.g. enhancements, needs-review)"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
const input = e.currentTarget;
|
||||
const label = input.value.trim();
|
||||
if (label) {
|
||||
field.onChange([
|
||||
...(field.value || []),
|
||||
label,
|
||||
]);
|
||||
input.value = "";
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
const input = document.querySelector(
|
||||
'input[placeholder*="Enter a label"]',
|
||||
) as HTMLInputElement;
|
||||
const label = input.value.trim();
|
||||
if (label) {
|
||||
field.onChange([...(field.value || []), label]);
|
||||
input.value = "";
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="previewLimit"
|
||||
@@ -312,6 +416,37 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="previewRequireCollaboratorPermissions"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm col-span-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>
|
||||
Require Collaborator Permissions
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
Require collaborator permissions to preview
|
||||
deployments, valid roles are:
|
||||
<ul>
|
||||
<li>Admin</li>
|
||||
<li>Maintain</li>
|
||||
<li>Write</li>
|
||||
</ul>
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="env"
|
||||
@@ -335,13 +470,37 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
{data?.buildType === "dockerfile" && (
|
||||
<Secrets
|
||||
name="buildArgs"
|
||||
title="Build-time Variables"
|
||||
title="Build-time Arguments"
|
||||
description={
|
||||
<span>
|
||||
Available only at build-time. See documentation
|
||||
Arguments are available only at build-time. See
|
||||
documentation
|
||||
<a
|
||||
className="text-primary"
|
||||
href="https://docs.docker.com/build/guide/build-args/"
|
||||
href="https://docs.docker.com/build/building/variables/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
}
|
||||
placeholder="NPM_TOKEN=xyz"
|
||||
/>
|
||||
)}
|
||||
{data?.buildType === "dockerfile" && (
|
||||
<Secrets
|
||||
name="buildSecrets"
|
||||
title="Build-time Secrets"
|
||||
description={
|
||||
<span>
|
||||
Secrets are specially designed for sensitive information
|
||||
and are only available at build-time. See
|
||||
documentation
|
||||
<a
|
||||
className="text-primary"
|
||||
href="https://docs.docker.com/build/building/secrets/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -18,11 +23,6 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const formSchema = z.object({
|
||||
rollbackActive: z.boolean(),
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
DatabaseZap,
|
||||
Info,
|
||||
PenBoxIcon,
|
||||
PlusCircle,
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { type Control, useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
@@ -34,18 +47,6 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
DatabaseZap,
|
||||
Info,
|
||||
PenBoxIcon,
|
||||
PlusCircle,
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import type { CacheType } from "../domains/handle-domain";
|
||||
|
||||
export const commonCronExpressions = [
|
||||
@@ -56,6 +57,7 @@ export const commonCronExpressions = [
|
||||
{ label: "Every month on the 1st at midnight", value: "0 0 1 * *" },
|
||||
{ label: "Every 15 minutes", value: "*/15 * * * *" },
|
||||
{ label: "Every weekday at midnight", value: "0 0 * * 1-5" },
|
||||
{ label: "Custom", value: "custom" },
|
||||
];
|
||||
|
||||
const formSchema = z
|
||||
@@ -114,10 +116,91 @@ interface Props {
|
||||
scheduleType?: "application" | "compose" | "server" | "dokploy-server";
|
||||
}
|
||||
|
||||
export const ScheduleFormField = ({
|
||||
name,
|
||||
formControl,
|
||||
}: {
|
||||
name: string;
|
||||
formControl: Control<any>;
|
||||
}) => {
|
||||
const [selectedOption, setSelectedOption] = useState("");
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={formControl}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center gap-2">
|
||||
Schedule
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Cron expression format: minute hour day month weekday</p>
|
||||
<p>Example: 0 0 * * * (daily at midnight)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</FormLabel>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Select
|
||||
value={selectedOption}
|
||||
onValueChange={(value) => {
|
||||
setSelectedOption(value);
|
||||
field.onChange(value === "custom" ? "" : value);
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a predefined schedule" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{commonCronExpressions.map((expr) => (
|
||||
<SelectItem key={expr.value} value={expr.value}>
|
||||
{expr.label}
|
||||
{expr.value !== "custom" && ` (${expr.value})`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="relative">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Custom cron expression (e.g., 0 0 * * *)"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
const commonExpression = commonCronExpressions.find(
|
||||
(expression) => expression.value === value,
|
||||
);
|
||||
if (commonExpression) {
|
||||
setSelectedOption(commonExpression.value);
|
||||
} else {
|
||||
setSelectedOption("custom");
|
||||
}
|
||||
field.onChange(e);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<FormDescription>
|
||||
Choose a predefined schedule or enter a custom cron expression
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
||||
|
||||
const utils = api.useUtils();
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
@@ -232,14 +315,17 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
className={cn(
|
||||
"max-h-screen overflow-y-auto",
|
||||
scheduleTypeForm === "dokploy-server" || scheduleTypeForm === "server"
|
||||
? "max-h-[95vh] sm:max-w-2xl"
|
||||
: " sm:max-w-lg",
|
||||
? "sm:max-w-2xl"
|
||||
: "sm:max-w-lg",
|
||||
)}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{scheduleId ? "Edit" : "Create"} Schedule</DialogTitle>
|
||||
<DialogDescription>
|
||||
{scheduleId ? "Manage" : "Create"} a schedule to run a task at a
|
||||
specific time or interval.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
@@ -373,63 +459,9 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
<ScheduleFormField
|
||||
name="cronExpression"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center gap-2">
|
||||
Schedule
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Cron expression format: minute hour day month
|
||||
weekday
|
||||
</p>
|
||||
<p>Example: 0 0 * * * (daily at midnight)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</FormLabel>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a predefined schedule" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{commonCronExpressions.map((expr) => (
|
||||
<SelectItem key={expr.value} value={expr.value}>
|
||||
{expr.label} ({expr.value})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="relative">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Custom cron expression (e.g., 0 0 * * *)"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<FormDescription>
|
||||
Choose a predefined schedule or enter a custom cron
|
||||
expression
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
formControl={form.control}
|
||||
/>
|
||||
|
||||
{(scheduleTypeForm === "application" ||
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
import {
|
||||
ClipboardList,
|
||||
Clock,
|
||||
Loader2,
|
||||
Play,
|
||||
Terminal,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -15,15 +25,6 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
ClipboardList,
|
||||
Clock,
|
||||
Loader2,
|
||||
Play,
|
||||
Terminal,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
|
||||
import { HandleSchedules } from "./handle-schedules";
|
||||
|
||||
@@ -33,6 +34,9 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
const [runningSchedules, setRunningSchedules] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
const {
|
||||
data: schedules,
|
||||
isLoading: isLoadingSchedules,
|
||||
@@ -46,19 +50,32 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
enabled: !!id,
|
||||
},
|
||||
);
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutateAsync: deleteSchedule, isLoading: isDeleting } =
|
||||
api.schedule.delete.useMutation();
|
||||
const { mutateAsync: runManually } = api.schedule.runManually.useMutation();
|
||||
|
||||
const { mutateAsync: runManually, isLoading } =
|
||||
api.schedule.runManually.useMutation();
|
||||
const handleRunManually = async (scheduleId: string) => {
|
||||
setRunningSchedules((prev) => new Set(prev).add(scheduleId));
|
||||
try {
|
||||
await runManually({ scheduleId });
|
||||
toast.success("Schedule run successfully");
|
||||
await refetchSchedules();
|
||||
} catch {
|
||||
toast.error("Error running schedule");
|
||||
} finally {
|
||||
setRunningSchedules((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(scheduleId);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
|
||||
<CardHeader className="px-0">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex justify-between items-center gap-y-2 flex-wrap">
|
||||
<div className="flex flex-col gap-2">
|
||||
<CardTitle className="text-xl font-bold flex items-center gap-2">
|
||||
Scheduled Tasks
|
||||
@@ -67,7 +84,6 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
Schedule tasks to run automatically at specified intervals.
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
{schedules && schedules.length > 0 && (
|
||||
<HandleSchedules id={id} scheduleType={scheduleType} />
|
||||
)}
|
||||
@@ -75,7 +91,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
</CardHeader>
|
||||
<CardContent className="px-0">
|
||||
{isLoadingSchedules ? (
|
||||
<div className="flex gap-4 w-full items-center justify-center text-center mx-auto min-h-[45vh]">
|
||||
<div className="flex gap-4 w-full items-center justify-center text-center mx-auto min-h-[45vh]">
|
||||
<Loader2 className="size-4 text-muted-foreground/70 transition-colors animate-spin self-center" />
|
||||
<span className="text-sm text-muted-foreground/70">
|
||||
Loading scheduled tasks...
|
||||
@@ -91,15 +107,15 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
return (
|
||||
<div
|
||||
key={schedule.scheduleId}
|
||||
className=" flex items-center justify-between rounded-lg border p-3 transition-colors bg-muted/50"
|
||||
className="flex flex-col sm:flex-row sm:items-center flex-wrap sm:flex-nowrap gap-y-2 justify-between rounded-lg border p-3 transition-colors bg-muted/50 w-full"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/5">
|
||||
<div className="flex items-start gap-3 w-full sm:w-auto">
|
||||
<div className="flex flex-shrink-0 h-9 w-9 items-center justify-center rounded-full bg-primary/5">
|
||||
<Clock className="size-4 text-primary/70" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-medium leading-none">
|
||||
<div className="space-y-1.5 w-full sm:w-auto">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h3 className="text-sm font-medium leading-none [overflow-wrap:anywhere] line-clamp-3">
|
||||
{schedule.name}
|
||||
</h3>
|
||||
<Badge
|
||||
@@ -109,7 +125,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
{schedule.enabled ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground flex-wrap">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="font-mono text-[10px] bg-transparent"
|
||||
@@ -132,27 +148,25 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
)}
|
||||
</div>
|
||||
{schedule.command && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="size-3.5 text-muted-foreground/70" />
|
||||
<code className="font-mono text-[10px] text-muted-foreground/70">
|
||||
<div className="flex items-start gap-2 max-w-full">
|
||||
<Terminal className="size-3.5 text-muted-foreground/70 flex-shrink-0 mt-0.5" />
|
||||
<code className="font-mono text-[10px] text-muted-foreground/70 break-all max-w-[calc(100%-20px)]">
|
||||
{schedule.command}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex items-center gap-0.5 md:gap-1.5">
|
||||
<ShowDeploymentsModal
|
||||
id={schedule.scheduleId}
|
||||
type="schedule"
|
||||
serverId={serverId || undefined}
|
||||
>
|
||||
<Button variant="ghost" size="icon">
|
||||
<ClipboardList className="size-4 transition-colors " />
|
||||
<ClipboardList className="size-4 transition-colors" />
|
||||
</Button>
|
||||
</ShowDeploymentsModal>
|
||||
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -160,37 +174,26 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
isLoading={isLoading}
|
||||
onClick={async () => {
|
||||
toast.success("Schedule run successfully");
|
||||
|
||||
await runManually({
|
||||
scheduleId: schedule.scheduleId,
|
||||
})
|
||||
.then(async () => {
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, 1500),
|
||||
);
|
||||
refetchSchedules();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error running schedule");
|
||||
});
|
||||
}}
|
||||
disabled={runningSchedules.has(schedule.scheduleId)}
|
||||
onClick={() =>
|
||||
handleRunManually(schedule.scheduleId)
|
||||
}
|
||||
>
|
||||
<Play className="size-4 transition-colors" />
|
||||
{runningSchedules.has(schedule.scheduleId) ? (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
) : (
|
||||
<Play className="size-4 transition-colors" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Run Manual Schedule</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<HandleSchedules
|
||||
scheduleId={schedule.scheduleId}
|
||||
id={id}
|
||||
scheduleType={scheduleType}
|
||||
/>
|
||||
|
||||
<DialogAction
|
||||
title="Delete Schedule"
|
||||
description="Are you sure you want to delete this schedule?"
|
||||
@@ -214,8 +217,8 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10 "
|
||||
isLoading={isDeleting}
|
||||
className="group hover:bg-red-500/10"
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
@@ -226,7 +229,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2 items-center justify-center py-12 rounded-lg">
|
||||
<div className="flex flex-col gap-2 items-center justify-center py-12 rounded-lg">
|
||||
<Clock className="size-8 mb-4 text-muted-foreground" />
|
||||
<p className="text-lg font-medium text-muted-foreground">
|
||||
No scheduled tasks
|
||||
|
||||
@@ -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 { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -20,12 +26,6 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PenBoxIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const updateApplicationSchema = z.object({
|
||||
name: z.string().min(1, {
|
||||
@@ -99,7 +99,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
|
||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Modify Application</DialogTitle>
|
||||
<DialogDescription>Update the application data</DialogDescription>
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { DatabaseZap, PenBoxIcon, PlusCircle, RefreshCw } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -34,20 +40,8 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
DatabaseZap,
|
||||
Info,
|
||||
PenBoxIcon,
|
||||
PlusCircle,
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import type { CacheType } from "../domains/handle-domain";
|
||||
import { commonCronExpressions } from "../schedules/handle-schedules";
|
||||
import { ScheduleFormField } from "../schedules/handle-schedules";
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
@@ -55,7 +49,12 @@ const formSchema = z
|
||||
cronExpression: z.string().min(1, "Cron expression is required"),
|
||||
volumeName: z.string().min(1, "Volume name is required"),
|
||||
prefix: z.string(),
|
||||
// keepLatestCount: z.coerce.number().optional(),
|
||||
keepLatestCount: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.gte(1, "Must be at least 1")
|
||||
.optional()
|
||||
.nullable(),
|
||||
turnOff: z.boolean().default(false),
|
||||
enabled: z.boolean().default(true),
|
||||
serviceType: z.enum([
|
||||
@@ -108,6 +107,7 @@ export const HandleVolumeBackups = ({
|
||||
}: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
||||
const [keepLatestCountInput, setKeepLatestCountInput] = useState("");
|
||||
|
||||
const utils = api.useUtils();
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
@@ -117,7 +117,7 @@ export const HandleVolumeBackups = ({
|
||||
cronExpression: "",
|
||||
volumeName: "",
|
||||
prefix: "",
|
||||
// keepLatestCount: undefined,
|
||||
keepLatestCount: undefined,
|
||||
turnOff: false,
|
||||
enabled: true,
|
||||
serviceName: "",
|
||||
@@ -173,13 +173,19 @@ export const HandleVolumeBackups = ({
|
||||
cronExpression: volumeBackup.cronExpression,
|
||||
volumeName: volumeBackup.volumeName || "",
|
||||
prefix: volumeBackup.prefix,
|
||||
// keepLatestCount: volumeBackup.keepLatestCount || undefined,
|
||||
keepLatestCount: volumeBackup.keepLatestCount || undefined,
|
||||
turnOff: volumeBackup.turnOff,
|
||||
enabled: volumeBackup.enabled || false,
|
||||
serviceName: volumeBackup.serviceName || "",
|
||||
destinationId: volumeBackup.destinationId,
|
||||
serviceType: volumeBackup.serviceType,
|
||||
});
|
||||
setKeepLatestCountInput(
|
||||
volumeBackup.keepLatestCount !== null &&
|
||||
volumeBackup.keepLatestCount !== undefined
|
||||
? String(volumeBackup.keepLatestCount)
|
||||
: "",
|
||||
);
|
||||
}
|
||||
}, [form, volumeBackup, volumeBackupId]);
|
||||
|
||||
@@ -190,8 +196,12 @@ export const HandleVolumeBackups = ({
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
if (!id && !volumeBackupId) return;
|
||||
|
||||
const preparedKeepLatestCount =
|
||||
keepLatestCountInput === "" ? null : (values.keepLatestCount ?? null);
|
||||
|
||||
await mutateAsync({
|
||||
...values,
|
||||
keepLatestCount: preparedKeepLatestCount,
|
||||
destinationId: values.destinationId,
|
||||
volumeBackupId: volumeBackupId || "",
|
||||
serviceType: volumeBackupType,
|
||||
@@ -257,9 +267,8 @@ export const HandleVolumeBackups = ({
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
className={cn(
|
||||
"max-h-screen overflow-y-auto",
|
||||
volumeBackupType === "compose" || volumeBackupType === "application"
|
||||
? "max-h-[95vh] sm:max-w-2xl"
|
||||
? "sm:max-w-2xl"
|
||||
: " sm:max-w-lg",
|
||||
)}
|
||||
>
|
||||
@@ -291,64 +300,9 @@ export const HandleVolumeBackups = ({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
<ScheduleFormField
|
||||
name="cronExpression"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center gap-2">
|
||||
Schedule
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Cron expression format: minute hour day month
|
||||
weekday
|
||||
</p>
|
||||
<p>Example: 0 0 * * * (daily at midnight)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</FormLabel>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a predefined schedule" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{commonCronExpressions.map((expr) => (
|
||||
<SelectItem key={expr.value} value={expr.value}>
|
||||
{expr.label} ({expr.value})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="relative">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Custom cron expression (e.g., 0 0 * * *)"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<FormDescription>
|
||||
Choose a predefined schedule or enter a custom cron
|
||||
expression
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
formControl={form.control}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
@@ -600,29 +554,38 @@ export const HandleVolumeBackups = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* <FormField
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="keepLatestCount"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Keep Latest Count</FormLabel>
|
||||
<FormLabel>Keep Latest Backups</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="5"
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(Number(e.target.value) || undefined)
|
||||
}
|
||||
type="number"
|
||||
min={1}
|
||||
autoComplete="off"
|
||||
placeholder="Leave empty to keep all"
|
||||
value={keepLatestCountInput}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value;
|
||||
setKeepLatestCountInput(raw);
|
||||
if (raw === "") {
|
||||
field.onChange(undefined);
|
||||
} else if (/^\d+$/.test(raw)) {
|
||||
field.onChange(Number(raw));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Number of backup files to keep (optional)
|
||||
How many recent backups to keep. Empty means no cleanup.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/> */}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { debounce } from "lodash";
|
||||
import { CheckIcon, ChevronsUpDown, Copy, RotateCcw } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { DrawerLogs } from "@/components/shared/drawer-logs";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -34,17 +43,8 @@ import {
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { debounce } from "lodash";
|
||||
import { CheckIcon, ChevronsUpDown, Copy, RotateCcw } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||
import { formatBytes } from "../../database/backups/restore-backup";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@@ -161,7 +161,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
|
||||
Restore Volume Backup
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center">
|
||||
<RotateCcw className="mr-2 size-4" />
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
import {
|
||||
ClipboardList,
|
||||
DatabaseBackup,
|
||||
Loader2,
|
||||
Play,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -15,16 +24,8 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
ClipboardList,
|
||||
DatabaseBackup,
|
||||
Loader2,
|
||||
Play,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { HandleVolumeBackups } from "./handle-volume-backups";
|
||||
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
|
||||
import { HandleVolumeBackups } from "./handle-volume-backups";
|
||||
import { RestoreVolumeBackups } from "./restore-volume-backups";
|
||||
|
||||
interface Props {
|
||||
@@ -38,6 +39,7 @@ export const ShowVolumeBackups = ({
|
||||
type = "application",
|
||||
serverId,
|
||||
}: Props) => {
|
||||
const [runningBackups, setRunningBackups] = useState<Set<string>>(new Set());
|
||||
const {
|
||||
data: volumeBackups,
|
||||
isLoading: isLoadingVolumeBackups,
|
||||
@@ -51,19 +53,33 @@ export const ShowVolumeBackups = ({
|
||||
enabled: !!id,
|
||||
},
|
||||
);
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutateAsync: deleteVolumeBackup, isLoading: isDeleting } =
|
||||
api.volumeBackups.delete.useMutation();
|
||||
|
||||
const { mutateAsync: runManually, isLoading } =
|
||||
const { mutateAsync: runManually } =
|
||||
api.volumeBackups.runManually.useMutation();
|
||||
|
||||
const handleRunManually = async (volumeBackupId: string) => {
|
||||
setRunningBackups((prev) => new Set(prev).add(volumeBackupId));
|
||||
try {
|
||||
await runManually({ volumeBackupId });
|
||||
toast.success("Volume backup run successfully");
|
||||
await refetchVolumeBackups();
|
||||
} catch {
|
||||
toast.error("Error running volume backup");
|
||||
} finally {
|
||||
setRunningBackups((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(volumeBackupId);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
|
||||
<CardHeader className="px-0">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex justify-between items-center flex-wrap gap-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<CardTitle className="text-xl font-bold flex items-center gap-2">
|
||||
Volume Backups
|
||||
@@ -73,12 +89,10 @@ export const ShowVolumeBackups = ({
|
||||
intervals.
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{volumeBackups && volumeBackups.length > 0 && (
|
||||
<>
|
||||
<HandleVolumeBackups id={id} volumeBackupType={type} />
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<RestoreVolumeBackups
|
||||
id={id}
|
||||
@@ -93,7 +107,7 @@ export const ShowVolumeBackups = ({
|
||||
</CardHeader>
|
||||
<CardContent className="px-0">
|
||||
{isLoadingVolumeBackups ? (
|
||||
<div className="flex gap-4 w-full items-center justify-center text-center mx-auto min-h-[45vh]">
|
||||
<div className="flex gap-4 w-full items-center justify-center text-center mx-auto min-h-[45vh]">
|
||||
<Loader2 className="size-4 text-muted-foreground/70 transition-colors animate-spin self-center" />
|
||||
<span className="text-sm text-muted-foreground/70">
|
||||
Loading volume backups...
|
||||
@@ -113,13 +127,13 @@ export const ShowVolumeBackups = ({
|
||||
return (
|
||||
<div
|
||||
key={volumeBackup.volumeBackupId}
|
||||
className=" flex items-center justify-between rounded-lg border p-3 transition-colors bg-muted/50"
|
||||
className="flex flex-col sm:flex-row sm:items-center flex-wrap sm:flex-nowrap gap-y-2 justify-between rounded-lg border p-3 transition-colors bg-muted/50 w-full"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex items-start gap-3 w-full sm:w-auto">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/5">
|
||||
<DatabaseBackup className="size-4 text-primary/70" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-1.5 w-full sm:w-auto">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-medium leading-none">
|
||||
{volumeBackup.name}
|
||||
@@ -143,18 +157,16 @@ export const ShowVolumeBackups = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex items-center gap-1.5 mt-2 sm:mt-0 sm:ml-3">
|
||||
<ShowDeploymentsModal
|
||||
id={volumeBackup.volumeBackupId}
|
||||
type="volumeBackup"
|
||||
serverId={serverId || undefined}
|
||||
>
|
||||
<Button variant="ghost" size="icon">
|
||||
<ClipboardList className="size-4 transition-colors " />
|
||||
<ClipboardList className="size-4 transition-colors" />
|
||||
</Button>
|
||||
</ShowDeploymentsModal>
|
||||
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -162,25 +174,18 @@ export const ShowVolumeBackups = ({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
isLoading={isLoading}
|
||||
onClick={async () => {
|
||||
toast.success("Volume backup run successfully");
|
||||
|
||||
await runManually({
|
||||
volumeBackupId: volumeBackup.volumeBackupId,
|
||||
})
|
||||
.then(async () => {
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, 1500),
|
||||
);
|
||||
refetchVolumeBackups();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error running volume backup");
|
||||
});
|
||||
}}
|
||||
disabled={runningBackups.has(
|
||||
volumeBackup.volumeBackupId,
|
||||
)}
|
||||
onClick={() =>
|
||||
handleRunManually(volumeBackup.volumeBackupId)
|
||||
}
|
||||
>
|
||||
<Play className="size-4 transition-colors" />
|
||||
{runningBackups.has(volumeBackup.volumeBackupId) ? (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
) : (
|
||||
<Play className="size-4 transition-colors" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
@@ -188,13 +193,11 @@ export const ShowVolumeBackups = ({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<HandleVolumeBackups
|
||||
volumeBackupId={volumeBackup.volumeBackupId}
|
||||
id={id}
|
||||
volumeBackupType={type}
|
||||
/>
|
||||
|
||||
<DialogAction
|
||||
title="Delete Volume Backup"
|
||||
description="Are you sure you want to delete this volume backup?"
|
||||
@@ -218,7 +221,7 @@ export const ShowVolumeBackups = ({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10 "
|
||||
className="group hover:bg-red-500/10"
|
||||
isLoading={isDeleting}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
@@ -230,7 +233,7 @@ export const ShowVolumeBackups = ({
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2 items-center justify-center py-12 rounded-lg">
|
||||
<div className="flex flex-col gap-2 items-center justify-center py-12 rounded-lg">
|
||||
<DatabaseBackup className="size-8 mb-4 text-muted-foreground" />
|
||||
<p className="text-lg font-medium text-muted-foreground">
|
||||
No volume backups
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -18,11 +23,7 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
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 {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, Loader2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
} from "@/components/ui/form";
|
||||
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
// Schema for Isolated Deployment
|
||||
const isolatedSchema = z.object({
|
||||
isolatedDeployment: z.boolean().optional(),
|
||||
});
|
||||
|
||||
type IsolatedSchema = z.infer<typeof isolatedSchema>;
|
||||
|
||||
export const IsolatedDeploymentTab = ({ composeId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [compose, setCompose] = useState<string>("");
|
||||
const [isPreviewLoading, setIsPreviewLoading] = useState<boolean>(false);
|
||||
const { mutateAsync, error, isError } =
|
||||
api.compose.isolatedDeployment.useMutation();
|
||||
|
||||
const [isOpenPreview, setIsOpenPreview] = useState<boolean>(false);
|
||||
|
||||
const { mutateAsync: updateCompose } = api.compose.update.useMutation();
|
||||
|
||||
const { data, refetch } = api.compose.one.useQuery(
|
||||
{ composeId },
|
||||
{ enabled: !!composeId },
|
||||
);
|
||||
|
||||
const form = useForm<IsolatedSchema>({
|
||||
defaultValues: {
|
||||
isolatedDeployment: false,
|
||||
},
|
||||
resolver: zodResolver(isolatedSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
isolatedDeployment: data?.isolatedDeployment || false,
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||
|
||||
const onSubmit = async (formData: IsolatedSchema) => {
|
||||
await updateCompose({
|
||||
composeId,
|
||||
isolatedDeployment: formData?.isolatedDeployment || false,
|
||||
})
|
||||
.then(async (_data) => {
|
||||
await refetch();
|
||||
toast.success("Compose updated");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating the compose");
|
||||
});
|
||||
};
|
||||
|
||||
const generatePreview = async () => {
|
||||
setIsOpenPreview(true);
|
||||
setIsPreviewLoading(true);
|
||||
try {
|
||||
await mutateAsync({
|
||||
composeId,
|
||||
suffix: data?.appName || "",
|
||||
}).then(async (data) => {
|
||||
await utils.project.all.invalidate();
|
||||
setCompose(data);
|
||||
});
|
||||
} catch {
|
||||
toast.error("Error generating preview");
|
||||
setIsOpenPreview(false);
|
||||
} finally {
|
||||
setIsPreviewLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Enable Isolated Deployment</CardTitle>
|
||||
<CardDescription>
|
||||
Configure isolated deployment to the compose file.
|
||||
<div className="text-sm text-muted-foreground flex flex-col gap-2">
|
||||
<span>
|
||||
This feature creates an isolated environment for your deployment
|
||||
by adding unique prefixes to all resources. It establishes a
|
||||
dedicated network based on your compose file's name, ensuring your
|
||||
services run in isolation. This prevents conflicts when running
|
||||
multiple instances of the same template or services with identical
|
||||
names.
|
||||
</span>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">
|
||||
Resources that will be isolated:
|
||||
</h4>
|
||||
<ul className="list-disc list-inside">
|
||||
<li>Docker networks</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
id="isolated-deployment-form"
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
{isError && (
|
||||
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
|
||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||
<span className="text-sm text-red-600 dark:text-red-400">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col lg:flex-col gap-4 w-full">
|
||||
<div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isolatedDeployment"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>
|
||||
Enable Isolated Deployment ({data?.appName})
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
Enable isolated deployment to the compose file.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
|
||||
<Button
|
||||
form="isolated-deployment-form"
|
||||
type="submit"
|
||||
className="lg:w-fit"
|
||||
isLoading={form.formState.isSubmitting}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
|
||||
<Button
|
||||
onClick={generatePreview}
|
||||
isLoading={isPreviewLoading}
|
||||
variant="secondary"
|
||||
className="lg:w-fit"
|
||||
>
|
||||
Preview Compose
|
||||
</Button>
|
||||
<Dialog open={isOpenPreview} onOpenChange={setIsOpenPreview}>
|
||||
<DialogContent className="sm:max-w-6xl max-h-[80vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Isolated Deployment Preview</DialogTitle>
|
||||
<DialogDescription>
|
||||
Preview of the compose file with isolated deployment
|
||||
configuration
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4 overflow-auto">
|
||||
{isPreviewLoading ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 gap-4">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
|
||||
<p className="text-muted-foreground">
|
||||
Generating compose preview...
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<pre>
|
||||
<CodeEditor
|
||||
value={compose || ""}
|
||||
language="yaml"
|
||||
readOnly
|
||||
height="60vh"
|
||||
/>
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,13 @@
|
||||
import type { ServiceType } from "@dokploy/server/db/schema";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { Copy, Trash2 } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
@@ -20,15 +30,6 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import type { ServiceType } from "@dokploy/server/db/schema";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { Copy, Trash2 } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const deleteComposeSchema = z.object({
|
||||
projectName: z.string().min(1, {
|
||||
@@ -100,8 +101,10 @@ export const DeleteService = ({ id, type }: Props) => {
|
||||
deleteVolumes,
|
||||
})
|
||||
.then((result) => {
|
||||
push(`/dashboard/project/${result?.projectId}`);
|
||||
toast.success("deleted successfully");
|
||||
push(
|
||||
`/dashboard/project/${result?.environment?.projectId}/environment/${result?.environment?.environmentId}`,
|
||||
);
|
||||
toast.success("Service deleted successfully");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -114,6 +117,12 @@ export const DeleteService = ({ id, type }: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const isDisabled =
|
||||
(data &&
|
||||
"applicationStatus" in data &&
|
||||
data?.applicationStatus === "running") ||
|
||||
(data && "composeStatus" in data && data?.composeStatus === "running");
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
@@ -126,7 +135,7 @@ export const DeleteService = ({ id, type }: Props) => {
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -202,6 +211,12 @@ export const DeleteService = ({ id, type }: Props) => {
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
{isDisabled && (
|
||||
<AlertBlock type="warning" className="w-full mt-5">
|
||||
Cannot delete the service while it is running. Please wait for the
|
||||
build to finish and then try again.
|
||||
</AlertBlock>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -211,8 +226,10 @@ export const DeleteService = ({ id, type }: Props) => {
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
disabled={isDisabled}
|
||||
form="hook-form-delete-compose"
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user