mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
642 Commits
v0.16.1
...
1365-creat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da0e726326 | ||
|
|
c89f957133 | ||
|
|
8ba3a42c1e | ||
|
|
2c3ff5794d | ||
|
|
673e0a6880 | ||
|
|
b64ddf1119 | ||
|
|
2f074ac734 | ||
|
|
96e3721b4b | ||
|
|
b8e5cae88f | ||
|
|
fa20444a14 | ||
|
|
668ccabec8 | ||
|
|
aa07a0c574 | ||
|
|
0b64b43376 | ||
|
|
5c65dc9a21 | ||
|
|
58262606d4 | ||
|
|
f73959db41 | ||
|
|
e6c664e65f | ||
|
|
36cc157566 | ||
|
|
7e070623cc | ||
|
|
b2c0a685f8 | ||
|
|
c14528886d | ||
|
|
29eb490e2d | ||
|
|
6166963b00 | ||
|
|
f544efed35 | ||
|
|
598d095241 | ||
|
|
457a8e05fd | ||
|
|
3ca057c44a | ||
|
|
ad3a0198e9 | ||
|
|
ab5f62604c | ||
|
|
bf9e886b9a | ||
|
|
f5cd0fbdd8 | ||
|
|
8859cc97b4 | ||
|
|
3bdd5e4dd0 | ||
|
|
b0c710aa92 | ||
|
|
c83d0a95b7 | ||
|
|
71ca5babfd | ||
|
|
f342613503 | ||
|
|
efd176451f | ||
|
|
a7fd64e019 | ||
|
|
21c8b98f9c | ||
|
|
6ff06576d0 | ||
|
|
24cc08a1ac | ||
|
|
e039826d50 | ||
|
|
a947b4915a | ||
|
|
fc1d9ad202 | ||
|
|
5489e3b0a5 | ||
|
|
e43b907a8d | ||
|
|
62fae661a1 | ||
|
|
6846e0e5a3 | ||
|
|
49d4cea06f | ||
|
|
5db7508530 | ||
|
|
4da4e1c17d | ||
|
|
126dad498c | ||
|
|
9c74b18e37 | ||
|
|
b13b906d75 | ||
|
|
f63d582530 | ||
|
|
0fa728d905 | ||
|
|
b56272871f | ||
|
|
1ffdebf60b | ||
|
|
0e81a027c1 | ||
|
|
cf3b3a2dcd | ||
|
|
a8fc27e830 | ||
|
|
e7db3a196c | ||
|
|
f78cda9cce | ||
|
|
747c2137c9 | ||
|
|
e6cb6454db | ||
|
|
819822f30b | ||
|
|
5b3005eb89 | ||
|
|
85a13eed00 | ||
|
|
e1b94dfe5b | ||
|
|
948fdbc22b | ||
|
|
eed38860b9 | ||
|
|
fefb5d14e0 | ||
|
|
8946f68af9 | ||
|
|
5fb2866660 | ||
|
|
c51d63a4df | ||
|
|
13eccaf8d9 | ||
|
|
adeb8498f9 | ||
|
|
43599e7a97 | ||
|
|
d7c94174b9 | ||
|
|
5c38a8265f | ||
|
|
a3362e0b15 | ||
|
|
0ad9233087 | ||
|
|
5dc5292928 | ||
|
|
5568629839 | ||
|
|
5aff345aa8 | ||
|
|
37ca8f41f5 | ||
|
|
cbec0603bd | ||
|
|
8c2707c4ea | ||
|
|
7d77e14319 | ||
|
|
d3c59ff8af | ||
|
|
7048e7e37e | ||
|
|
0a6382a731 | ||
|
|
d3b2cee7fb | ||
|
|
125e44812b | ||
|
|
ac3378ccb8 | ||
|
|
81e1161ba0 | ||
|
|
9aa13c5ac3 | ||
|
|
398fd54815 | ||
|
|
7f4e4ab8d2 | ||
|
|
211697acaf | ||
|
|
c0b64c6e55 | ||
|
|
5871a91da5 | ||
|
|
f4d13c3030 | ||
|
|
e00e19ec01 | ||
|
|
c995268b39 | ||
|
|
c8828b5620 | ||
|
|
ddd3101aeb | ||
|
|
51310dae1d | ||
|
|
0b7996adde | ||
|
|
fb4b507250 | ||
|
|
1294c2ad8e | ||
|
|
733f9a0024 | ||
|
|
73d3b58867 | ||
|
|
0ea138571d | ||
|
|
b1e7ffea21 | ||
|
|
c0a7347ef5 | ||
|
|
579faf2f58 | ||
|
|
7429a1f65f | ||
|
|
716c1db799 | ||
|
|
9dd7f51eeb | ||
|
|
4a1a5a9bb1 | ||
|
|
87836d23c3 | ||
|
|
30cbad93d2 | ||
|
|
038b021043 | ||
|
|
0ec8e2baa1 | ||
|
|
3f2722f28d | ||
|
|
47f7648cb3 | ||
|
|
0478419f7c | ||
|
|
b00c12965a | ||
|
|
8ab6d6b282 | ||
|
|
2470d672d4 | ||
|
|
3403f8ab36 | ||
|
|
1a415b96c9 | ||
|
|
81a881b07e | ||
|
|
baf555af52 | ||
|
|
c52725420e | ||
|
|
b02195db17 | ||
|
|
5ae103e779 | ||
|
|
a317f0c4cc | ||
|
|
24c9d3f7ad | ||
|
|
63638bde33 | ||
|
|
725bd1a381 | ||
|
|
790894ab93 | ||
|
|
5a1145996d | ||
|
|
a9e12c2b18 | ||
|
|
c8b1fd36bd | ||
|
|
609fea7daa | ||
|
|
b73e4102dd | ||
|
|
c7d47a6003 | ||
|
|
8c28223343 | ||
|
|
7abe060fcf | ||
|
|
56af89468a | ||
|
|
b1502f5f82 | ||
|
|
c78b8cfead | ||
|
|
0e8e92c715 | ||
|
|
e1632cbdb3 | ||
|
|
48c4ec55f9 | ||
|
|
90156da570 | ||
|
|
9856502ece | ||
|
|
a8d1471b16 | ||
|
|
27736c7c97 | ||
|
|
e7db0ccb70 | ||
|
|
4a1a14aeb4 | ||
|
|
ed62b4e1a3 | ||
|
|
515d65d993 | ||
|
|
2ebb02dc20 | ||
|
|
78c72b6337 | ||
|
|
e3e35ce792 | ||
|
|
6d0e195a4d | ||
|
|
53ce5e57fa | ||
|
|
87b12ff6e9 | ||
|
|
8b71f963cc | ||
|
|
1c5cc5a0db | ||
|
|
d233f2c764 | ||
|
|
1bbb4c9b64 | ||
|
|
9ca3ab3845 | ||
|
|
0baf9d8962 | ||
|
|
71c609df0e | ||
|
|
450302b2c2 | ||
|
|
71a007a4b3 | ||
|
|
871931b460 | ||
|
|
3a7bb5016c | ||
|
|
23b59076b8 | ||
|
|
2ddfc83f36 | ||
|
|
ba54124a56 | ||
|
|
64bdf07dbd | ||
|
|
8366219266 | ||
|
|
f38fb96eaf | ||
|
|
3b1ade804f | ||
|
|
fd69b45e5e | ||
|
|
6ec60b6bab | ||
|
|
12034e460e | ||
|
|
2b5a1d90b0 | ||
|
|
e7195c8acf | ||
|
|
9ace0f38cd | ||
|
|
796e50ed5f | ||
|
|
55abac3f2f | ||
|
|
b6c29ccf05 | ||
|
|
ca217affe6 | ||
|
|
ed54df9bd2 | ||
|
|
a4242d45d3 | ||
|
|
121755d9cc | ||
|
|
bf6c2698d4 | ||
|
|
bbdda014d8 | ||
|
|
316dfa6b2e | ||
|
|
e228325e37 | ||
|
|
d9c83b7010 | ||
|
|
5c24281f72 | ||
|
|
bc901bcb25 | ||
|
|
7c0d223e17 | ||
|
|
74ee024cf9 | ||
|
|
140a871275 | ||
|
|
d1f72a2e20 | ||
|
|
0d525398a8 | ||
|
|
7c62408070 | ||
|
|
23f1ce17de | ||
|
|
60eee55f2d | ||
|
|
209029214e | ||
|
|
9de3bf3c32 | ||
|
|
2c65fc22b0 | ||
|
|
6688b14753 | ||
|
|
6ea2a82bb0 | ||
|
|
8f562eefc1 | ||
|
|
e01347673e | ||
|
|
6179cef1ee | ||
|
|
b7112b89fd | ||
|
|
030c8a312d | ||
|
|
1db6ba94f4 | ||
|
|
afd3d2eea3 | ||
|
|
8bd72a8a34 | ||
|
|
fafc238e70 | ||
|
|
c04bf3c7e0 | ||
|
|
6b9fd596e5 | ||
|
|
7e36433144 | ||
|
|
0a6554c275 | ||
|
|
fcc55355f2 | ||
|
|
78e606876a | ||
|
|
7e99baa267 | ||
|
|
92c03bb7cc | ||
|
|
3a5ecb2f64 | ||
|
|
c0a00f4957 | ||
|
|
a8f94540f9 | ||
|
|
3e2cfe6eb8 | ||
|
|
b2d5090b36 | ||
|
|
0a0f53e9de | ||
|
|
17ce03e529 | ||
|
|
f44512a437 | ||
|
|
8379068fe3 | ||
|
|
a71de72a3c | ||
|
|
b024060eed | ||
|
|
56b26ce0d5 | ||
|
|
a9e3a65782 | ||
|
|
7a472df753 | ||
|
|
bd809c8dca | ||
|
|
48642979c5 | ||
|
|
46411a5f4e | ||
|
|
82cf0643d7 | ||
|
|
65780ee852 | ||
|
|
9d988c9a9b | ||
|
|
eb211b933e | ||
|
|
20eb6d7985 | ||
|
|
d424524d69 | ||
|
|
6f2148c060 | ||
|
|
97b77e526d | ||
|
|
077f47f2d8 | ||
|
|
5d6847b970 | ||
|
|
2630a73bd8 | ||
|
|
df5ef4a34e | ||
|
|
48a8c6021d | ||
|
|
d84a22fa72 | ||
|
|
1661022d56 | ||
|
|
2a295d6492 | ||
|
|
51851567db | ||
|
|
d1aaeb9a7b | ||
|
|
f9b4035c20 | ||
|
|
d492ff87f2 | ||
|
|
f638f49ab6 | ||
|
|
d1610855bb | ||
|
|
0c8c0844b1 | ||
|
|
98b19bb433 | ||
|
|
d8c5244d19 | ||
|
|
7bb8456cb7 | ||
|
|
74a0f5e992 | ||
|
|
8c69d2a085 | ||
|
|
c8a4a826ca | ||
|
|
e1b114a63b | ||
|
|
0b4d19abd6 | ||
|
|
1c0f6a8e60 | ||
|
|
c41aa0ccf7 | ||
|
|
96bb72eb99 | ||
|
|
ee2fed07b2 | ||
|
|
af083ffa5d | ||
|
|
f14ed5170d | ||
|
|
cd1c7e60bf | ||
|
|
79c6b7389c | ||
|
|
e48f1431a9 | ||
|
|
c2ecdb2d76 | ||
|
|
5c889e81a9 | ||
|
|
407e2e1137 | ||
|
|
2c6c89e4c1 | ||
|
|
41a8014585 | ||
|
|
fffe1d6249 | ||
|
|
cf0f5c8b97 | ||
|
|
777980618f | ||
|
|
dcd1df31c7 | ||
|
|
7369b54f32 | ||
|
|
009859faa9 | ||
|
|
f7a29accb1 | ||
|
|
50c518a63a | ||
|
|
79fca72d06 | ||
|
|
18c6d08b9a | ||
|
|
208094cd3e | ||
|
|
1342f73a02 | ||
|
|
1787c524f0 | ||
|
|
5899dc9394 | ||
|
|
6b48c0f354 | ||
|
|
33f3d1d87e | ||
|
|
4bfb172373 | ||
|
|
6cf96df6ec | ||
|
|
62a3707c10 | ||
|
|
00d2b3b572 | ||
|
|
d8f1548076 | ||
|
|
de4d1c0911 | ||
|
|
b96169fa55 | ||
|
|
e21e0e1865 | ||
|
|
dc9a194bbe | ||
|
|
27738d253e | ||
|
|
d37bde00bc | ||
|
|
55fae23ce3 | ||
|
|
d43cd52762 | ||
|
|
094491ecbf | ||
|
|
3d5bed0915 | ||
|
|
23de14f0b4 | ||
|
|
215811ae82 | ||
|
|
34b4956630 | ||
|
|
7ed0282ce1 | ||
|
|
24327139b8 | ||
|
|
0fb67ced5d | ||
|
|
1e56364f93 | ||
|
|
f980e459d9 | ||
|
|
3209550edc | ||
|
|
9835ead2b9 | ||
|
|
73f93f8a13 | ||
|
|
46ddaa68fa | ||
|
|
64e68cfde1 | ||
|
|
36c1a615c6 | ||
|
|
eeb97645b5 | ||
|
|
9a052c4657 | ||
|
|
165cdd27da | ||
|
|
13551f6065 | ||
|
|
f2ad1c5a57 | ||
|
|
4d5565895c | ||
|
|
8e51dedb78 | ||
|
|
780fa6b9cd | ||
|
|
95e642e63a | ||
|
|
5801c69034 | ||
|
|
2c98b4267f | ||
|
|
1874ffaa55 | ||
|
|
11c4101dc3 | ||
|
|
4c3b5ef271 | ||
|
|
4105353109 | ||
|
|
8a971072e4 | ||
|
|
dab12d6162 | ||
|
|
8d31574e5f | ||
|
|
0012ca7357 | ||
|
|
1aed53e6fe | ||
|
|
d6b966cfea | ||
|
|
772341fb1e | ||
|
|
e7a6247297 | ||
|
|
b1beb7b71b | ||
|
|
6254644fa6 | ||
|
|
bcb86f3d6d | ||
|
|
b96103247a | ||
|
|
87697147da | ||
|
|
074e3b6ec6 | ||
|
|
7061e06736 | ||
|
|
de35812d5a | ||
|
|
c5ac5f84b1 | ||
|
|
2e42fa7014 | ||
|
|
0fe8a1a221 | ||
|
|
829e77a6b8 | ||
|
|
adfe598671 | ||
|
|
ac49235916 | ||
|
|
d7210e9d7b | ||
|
|
52ea9ffa67 | ||
|
|
fd570ff861 | ||
|
|
130567dd78 | ||
|
|
0a826fbf1c | ||
|
|
081d08c20a | ||
|
|
41cf3d7b77 | ||
|
|
b05af62b7b | ||
|
|
c6569f70e4 | ||
|
|
d5c9338f51 | ||
|
|
dba39b6364 | ||
|
|
22de0fef49 | ||
|
|
03e1c17675 | ||
|
|
6edd2a81e5 | ||
|
|
fe5b8782e9 | ||
|
|
71f28fae70 | ||
|
|
c44618aa95 | ||
|
|
c7d86dd430 | ||
|
|
e50bbd1a6a | ||
|
|
d5ff91563a | ||
|
|
210fe0759c | ||
|
|
edff66900e | ||
|
|
4cf2177928 | ||
|
|
4a8306b015 | ||
|
|
f92d6693c3 | ||
|
|
81248ed03f | ||
|
|
c7d5900e11 | ||
|
|
d0608f43a9 | ||
|
|
adaf12a9a4 | ||
|
|
baa2ca20f4 | ||
|
|
537caf02e5 | ||
|
|
02ff507094 | ||
|
|
53df7d969e | ||
|
|
9e6e68558a | ||
|
|
a2e9ea2986 | ||
|
|
026e1bece6 | ||
|
|
51f6e08e16 | ||
|
|
c0f8218ad9 | ||
|
|
35dd6bff7a | ||
|
|
8dad8fd008 | ||
|
|
52dbc0d8f1 | ||
|
|
692f883064 | ||
|
|
6d90e268f7 | ||
|
|
1d86f1a0fc | ||
|
|
c7338983b8 | ||
|
|
ce06cd42b3 | ||
|
|
1a44a0ea5a | ||
|
|
444121f8d8 | ||
|
|
05a75edbec | ||
|
|
f5d81f434c | ||
|
|
10353d1f29 | ||
|
|
6226c75959 | ||
|
|
42c9cd5901 | ||
|
|
49edcdb99e | ||
|
|
ad71e8b36a | ||
|
|
b166cd5bfa | ||
|
|
0045608acc | ||
|
|
b140e81210 | ||
|
|
9298d6c693 | ||
|
|
23df3fba85 | ||
|
|
93c7f1ce76 | ||
|
|
1323394589 | ||
|
|
4f11fc2547 | ||
|
|
6d052ad455 | ||
|
|
60748da144 | ||
|
|
1956836cde | ||
|
|
7dca4fe430 | ||
|
|
84690c5f75 | ||
|
|
95b67ef2e9 | ||
|
|
3f8bc47ce5 | ||
|
|
498678c4ae | ||
|
|
3d602c232d | ||
|
|
94ffa7d578 | ||
|
|
64fc3c7677 | ||
|
|
f27830daf0 | ||
|
|
3ec2e2dd1a | ||
|
|
1e006cb094 | ||
|
|
8aa655af2c | ||
|
|
65659e27f1 | ||
|
|
4b6f9108d4 | ||
|
|
1e4a41a8e3 | ||
|
|
539aa7a85b | ||
|
|
b6ae502b92 | ||
|
|
e82db47ec4 | ||
|
|
f9b4f008c2 | ||
|
|
adb204ec1f | ||
|
|
5310a559b0 | ||
|
|
43b7db00f9 | ||
|
|
52c83fd6fc | ||
|
|
25a8df567e | ||
|
|
bda9b05134 | ||
|
|
e7329a727f | ||
|
|
e68465f9e6 | ||
|
|
5e7d344110 | ||
|
|
87546b4558 | ||
|
|
08ab18eebf | ||
|
|
ad642ab4e0 | ||
|
|
d5d8064b38 | ||
|
|
089274492d | ||
|
|
65c0ea829f | ||
|
|
d060eec465 | ||
|
|
2dca0d343e | ||
|
|
a8f63bb4d3 | ||
|
|
cecd371988 | ||
|
|
6c4d94cb4f | ||
|
|
c4e5c818f3 | ||
|
|
31a35d91e8 | ||
|
|
74a2f79a36 | ||
|
|
a8f8a727bd | ||
|
|
e8f2ab35c9 | ||
|
|
9806a5d607 | ||
|
|
e25d0c0c68 | ||
|
|
1f8a476264 | ||
|
|
cc473b3e87 | ||
|
|
10b3543d39 | ||
|
|
0893149db0 | ||
|
|
e257f86194 | ||
|
|
a9a0b4cb03 | ||
|
|
c5073c9f30 | ||
|
|
34ab01fcae | ||
|
|
df43f8318a | ||
|
|
e69c602d1c | ||
|
|
0116d995d9 | ||
|
|
ad479c4be1 | ||
|
|
f70192a71c | ||
|
|
abff70f081 | ||
|
|
9f03faaec2 | ||
|
|
1d760bd25f | ||
|
|
26a67dd175 | ||
|
|
013ee89a56 | ||
|
|
c3d3c7b96a | ||
|
|
4e724d88aa | ||
|
|
351e4b6103 | ||
|
|
5b633ec6c5 | ||
|
|
a46cbf4f2c | ||
|
|
d98fc82fbf | ||
|
|
ca9552d618 | ||
|
|
23b40c1aa7 | ||
|
|
5e0b7ba143 | ||
|
|
cb1203e302 | ||
|
|
373c5bc507 | ||
|
|
ee5afa4793 | ||
|
|
7b9426586d | ||
|
|
772b24a415 | ||
|
|
1922437115 | ||
|
|
c5eb8b087d | ||
|
|
94ee5391fb | ||
|
|
b3ff14f792 | ||
|
|
b68273c8ca | ||
|
|
8b203c48b4 | ||
|
|
fb33a5b6a5 | ||
|
|
b17ab6d8cb | ||
|
|
30d20bd267 | ||
|
|
f9b1c2575e | ||
|
|
b0b22224c3 | ||
|
|
2de0e73284 | ||
|
|
537950dd9f | ||
|
|
f2f3986c56 | ||
|
|
dd3fccea02 | ||
|
|
5052688aaf | ||
|
|
5825b3eae7 | ||
|
|
dbca102178 | ||
|
|
32a757a247 | ||
|
|
8e4d8d68ca | ||
|
|
48e6c40a38 | ||
|
|
0a06c0aa28 | ||
|
|
a965c0e924 | ||
|
|
ae97595610 | ||
|
|
106a660a2b | ||
|
|
c25e7c53aa | ||
|
|
c9308aebc2 | ||
|
|
c0a2d2c399 | ||
|
|
a104867ed2 | ||
|
|
87f4c7b71b | ||
|
|
d2094d6d76 | ||
|
|
c0b8a411bd | ||
|
|
1d8db07fa1 | ||
|
|
dd3618bfd9 | ||
|
|
9db979e43f | ||
|
|
553ae70656 | ||
|
|
b58b6636e3 | ||
|
|
15fd663972 | ||
|
|
123605dc0d | ||
|
|
d3f3265728 | ||
|
|
1a956314f8 | ||
|
|
15cc8440f2 | ||
|
|
4a9d7225c9 | ||
|
|
103654d678 | ||
|
|
34a375776e | ||
|
|
644fb1ef43 | ||
|
|
4e581bae99 | ||
|
|
ee9f4796c3 | ||
|
|
41a970c526 | ||
|
|
76c6d02566 | ||
|
|
d0a5427c66 | ||
|
|
aa3541b67b | ||
|
|
c31ed2b2b0 | ||
|
|
a42b0ba32b | ||
|
|
6866da97dd | ||
|
|
fdaea01b5b | ||
|
|
fa2d81d2e7 | ||
|
|
332416b7e7 | ||
|
|
7a4ee76eb6 | ||
|
|
0c304bd304 | ||
|
|
8314c1a0a5 | ||
|
|
46a4e31e71 | ||
|
|
acd3e0dd7d | ||
|
|
f4e7a4d79a | ||
|
|
c7e5eba086 | ||
|
|
65361d18c2 | ||
|
|
bb5fd9895d | ||
|
|
49a6b72c60 | ||
|
|
84a88299ea | ||
|
|
d0fe635620 | ||
|
|
41d4ff8489 | ||
|
|
b9d8a48ae6 | ||
|
|
3e8708d2b9 | ||
|
|
2b0232a24c | ||
|
|
cbdea7cf48 | ||
|
|
f042cb720f | ||
|
|
02b977bfc4 | ||
|
|
9f24f24de3 | ||
|
|
cbc8c24985 | ||
|
|
b17369264c | ||
|
|
9c783177c8 | ||
|
|
f2159a3439 | ||
|
|
eec0a55212 | ||
|
|
16bab629de | ||
|
|
592bf9292e | ||
|
|
b93d26f937 | ||
|
|
c0e7f4ad8c | ||
|
|
05cf51bfb3 | ||
|
|
7cf5cb4032 | ||
|
|
76787b9056 | ||
|
|
513b17105e | ||
|
|
187f051484 | ||
|
|
2be79304fb | ||
|
|
29a8cb63c0 | ||
|
|
ed5936ede7 | ||
|
|
d4f89425db | ||
|
|
a7983f32a6 | ||
|
|
198ace236b | ||
|
|
931f3fc28e | ||
|
|
a4cc3b619a | ||
|
|
cb5077cfcc | ||
|
|
9122a1e4b2 | ||
|
|
b2cf442d9b | ||
|
|
79b733536f | ||
|
|
65ee0a3e22 | ||
|
|
c9b570e469 | ||
|
|
dafed3096f | ||
|
|
f772fec407 | ||
|
|
06cbd1fce1 | ||
|
|
9c355bcfb7 | ||
|
|
06081627e8 | ||
|
|
dc1e12d6ed | ||
|
|
cb02deb837 | ||
|
|
94786c738b |
@@ -1,119 +0,0 @@
|
||||
version: 2.1
|
||||
|
||||
jobs:
|
||||
build-amd64:
|
||||
machine:
|
||||
image: ubuntu-2004:current
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Prepare .env file
|
||||
command: |
|
||||
cp apps/dokploy/.env.production.example .env.production
|
||||
cp apps/dokploy/.env.production.example apps/dokploy/.env.production
|
||||
|
||||
- run:
|
||||
name: Build and push AMD64 image
|
||||
command: |
|
||||
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
||||
if [ "${CIRCLE_BRANCH}" == "main" ]; then
|
||||
TAG="latest"
|
||||
elif [ "${CIRCLE_BRANCH}" == "canary" ]; then
|
||||
TAG="canary"
|
||||
else
|
||||
TAG="feature"
|
||||
fi
|
||||
docker build --platform linux/amd64 -t dokploy/dokploy:${TAG}-amd64 .
|
||||
docker push dokploy/dokploy:${TAG}-amd64
|
||||
|
||||
build-arm64:
|
||||
machine:
|
||||
image: ubuntu-2004:current
|
||||
resource_class: arm.large
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Prepare .env file
|
||||
command: |
|
||||
cp apps/dokploy/.env.production.example .env.production
|
||||
cp apps/dokploy/.env.production.example apps/dokploy/.env.production
|
||||
- run:
|
||||
name: Build and push ARM64 image
|
||||
command: |
|
||||
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
||||
if [ "${CIRCLE_BRANCH}" == "main" ]; then
|
||||
TAG="latest"
|
||||
elif [ "${CIRCLE_BRANCH}" == "canary" ]; then
|
||||
TAG="canary"
|
||||
else
|
||||
TAG="feature"
|
||||
fi
|
||||
docker build --platform linux/arm64 -t dokploy/dokploy:${TAG}-arm64 .
|
||||
docker push dokploy/dokploy:${TAG}-arm64
|
||||
|
||||
combine-manifests:
|
||||
docker:
|
||||
- image: cimg/node:18.18.0
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
- run:
|
||||
name: Create and push multi-arch manifest
|
||||
command: |
|
||||
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
||||
|
||||
if [ "${CIRCLE_BRANCH}" == "main" ]; then
|
||||
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
|
||||
echo $VERSION
|
||||
TAG="latest"
|
||||
|
||||
docker manifest create dokploy/dokploy:${TAG} \
|
||||
dokploy/dokploy:${TAG}-amd64 \
|
||||
dokploy/dokploy:${TAG}-arm64
|
||||
docker manifest push dokploy/dokploy:${TAG}
|
||||
|
||||
docker manifest create dokploy/dokploy:${VERSION} \
|
||||
dokploy/dokploy:${TAG}-amd64 \
|
||||
dokploy/dokploy:${TAG}-arm64
|
||||
docker manifest push dokploy/dokploy:${VERSION}
|
||||
elif [ "${CIRCLE_BRANCH}" == "canary" ]; then
|
||||
TAG="canary"
|
||||
docker manifest create dokploy/dokploy:${TAG} \
|
||||
dokploy/dokploy:${TAG}-amd64 \
|
||||
dokploy/dokploy:${TAG}-arm64
|
||||
docker manifest push dokploy/dokploy:${TAG}
|
||||
else
|
||||
TAG="feature"
|
||||
docker manifest create dokploy/dokploy:${TAG} \
|
||||
dokploy/dokploy:${TAG}-amd64 \
|
||||
dokploy/dokploy:${TAG}-arm64
|
||||
docker manifest push dokploy/dokploy:${TAG}
|
||||
fi
|
||||
|
||||
workflows:
|
||||
build-all:
|
||||
jobs:
|
||||
- build-amd64:
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
- canary
|
||||
- fix/nixpacks-version
|
||||
- build-arm64:
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
- canary
|
||||
- fix/nixpacks-version
|
||||
- combine-manifests:
|
||||
requires:
|
||||
- build-amd64
|
||||
- build-arm64
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
- canary
|
||||
- fix/nixpacks-version
|
||||
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Bug Report
|
||||
description: Create a bug report
|
||||
labels: ["bug"]
|
||||
labels: ["needs-triage🔍"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -62,6 +62,7 @@ body:
|
||||
- "Docker"
|
||||
- "Remote server"
|
||||
- "Local Development"
|
||||
- "Cloud Version"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
BIN
.github/sponsors/its.png
vendored
Normal file
BIN
.github/sponsors/its.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
.github/sponsors/light-node.webp
vendored
Normal file
BIN
.github/sponsors/light-node.webp
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
BIN
.github/sponsors/openalternative.png
vendored
Normal file
BIN
.github/sponsors/openalternative.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
BIN
.github/sponsors/synexa.png
vendored
Normal file
BIN
.github/sponsors/synexa.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
83
.github/workflows/create-pr.yml
vendored
Normal file
83
.github/workflows/create-pr.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
name: Auto PR to main when version changes
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- canary
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
create-pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
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"
|
||||
else
|
||||
VERSION_CHANGED="false"
|
||||
fi
|
||||
echo "VERSION_CHANGED=$VERSION_CHANGED" >> $GITHUB_ENV
|
||||
echo "Comparing versions:"
|
||||
echo "Current version: ${{ env.VERSION }}"
|
||||
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
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT }}
|
||||
|
||||
- name: Create Pull Request
|
||||
if: env.VERSION_CHANGED == 'true' && env.PR_EXISTS == '0'
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
git fetch origin main
|
||||
git checkout canary
|
||||
git push origin canary
|
||||
|
||||
gh pr create \
|
||||
--title "🚀 Release ${{ env.VERSION }}" \
|
||||
--body '
|
||||
This PR promotes changes from `canary` to `main` for version ${{ env.VERSION }}.
|
||||
|
||||
### 🔍 Changes Include:
|
||||
- Version bump to ${{ env.VERSION }}
|
||||
- All changes from canary branch
|
||||
|
||||
### ✅ Pre-merge Checklist:
|
||||
- [ ] All tests passing
|
||||
- [ ] Documentation updated
|
||||
- [ ] Docker images built and tested
|
||||
|
||||
> 🤖 This PR was automatically generated by [GitHub Actions](https://github.com/actions)' \
|
||||
--base main \
|
||||
--head canary \
|
||||
--label "release" --label "automated pr" || true \
|
||||
--reviewer siumauricio \
|
||||
--assignee siumauricio
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
9
.github/workflows/deploy.yml
vendored
9
.github/workflows/deploy.yml
vendored
@@ -2,7 +2,7 @@ name: Build Docker images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["canary", "main"]
|
||||
branches: ["canary", "main", "feat/monitoring"]
|
||||
|
||||
jobs:
|
||||
build-and-push-cloud-image:
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
@@ -53,8 +53,7 @@ jobs:
|
||||
push: true
|
||||
tags: |
|
||||
siumauricio/schedule:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
|
||||
platforms: linux/amd64
|
||||
|
||||
platforms: linux/amd64
|
||||
|
||||
build-and-push-server-image:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -77,4 +76,4 @@ jobs:
|
||||
push: true
|
||||
tags: |
|
||||
siumauricio/server:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64
|
||||
|
||||
161
.github/workflows/dokploy.yml
vendored
Normal file
161
.github/workflows/dokploy.yml
vendored
Normal file
@@ -0,0 +1,161 @@
|
||||
name: Dokploy Docker Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, canary, "feat/better-auth-2"]
|
||||
|
||||
env:
|
||||
IMAGE_NAME: dokploy/dokploy
|
||||
|
||||
jobs:
|
||||
docker-amd:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set tag and version
|
||||
id: meta
|
||||
run: |
|
||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||
TAG="latest"
|
||||
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
|
||||
elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then
|
||||
TAG="canary"
|
||||
else
|
||||
TAG="feature"
|
||||
fi
|
||||
echo "tags=${IMAGE_NAME}:${TAG}-amd64" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Prepare env file
|
||||
run: |
|
||||
cp apps/dokploy/.env.production.example .env.production
|
||||
cp apps/dokploy/.env.production.example apps/dokploy/.env.production
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
docker-arm:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set tag and version
|
||||
id: meta
|
||||
run: |
|
||||
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
|
||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||
TAG="latest"
|
||||
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
|
||||
elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then
|
||||
TAG="canary"
|
||||
else
|
||||
TAG="feature"
|
||||
fi
|
||||
echo "tags=${IMAGE_NAME}:${TAG}-arm64" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Prepare env file
|
||||
run: |
|
||||
cp apps/dokploy/.env.production.example .env.production
|
||||
cp apps/dokploy/.env.production.example apps/dokploy/.env.production
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
combine-manifests:
|
||||
needs: [docker-amd, docker-arm]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Create and push manifests
|
||||
run: |
|
||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
|
||||
TAG="latest"
|
||||
|
||||
docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \
|
||||
${IMAGE_NAME}:${TAG}-amd64 \
|
||||
${IMAGE_NAME}:${TAG}-arm64
|
||||
|
||||
docker buildx imagetools create -t ${IMAGE_NAME}:${VERSION} \
|
||||
${IMAGE_NAME}:${TAG}-amd64 \
|
||||
${IMAGE_NAME}:${TAG}-arm64
|
||||
|
||||
elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then
|
||||
TAG="canary"
|
||||
docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \
|
||||
${IMAGE_NAME}:${TAG}-amd64 \
|
||||
${IMAGE_NAME}:${TAG}-arm64
|
||||
|
||||
else
|
||||
TAG="feature"
|
||||
docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \
|
||||
${IMAGE_NAME}:${TAG}-amd64 \
|
||||
${IMAGE_NAME}:${TAG}-arm64
|
||||
fi
|
||||
|
||||
generate-release:
|
||||
needs: [combine-manifests]
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get version
|
||||
id: get_version
|
||||
run: |
|
||||
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.get_version.outputs.version }}
|
||||
name: ${{ steps.get_version.outputs.version }}
|
||||
generate_release_notes: true
|
||||
draft: false
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
118
.github/workflows/monitoring.yml
vendored
Normal file
118
.github/workflows/monitoring.yml
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
name: Dokploy Monitoring Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, canary]
|
||||
|
||||
env:
|
||||
IMAGE_NAME: dokploy/monitoring
|
||||
|
||||
jobs:
|
||||
docker-amd:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set tag
|
||||
id: meta
|
||||
run: |
|
||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||
TAG="latest"
|
||||
elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then
|
||||
TAG="canary"
|
||||
else
|
||||
TAG="feature"
|
||||
fi
|
||||
echo "tags=${IMAGE_NAME}:${TAG}-amd64" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.monitoring
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
docker-arm:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set
|
||||
id: meta
|
||||
run: |
|
||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||
TAG="latest"
|
||||
elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then
|
||||
TAG="canary"
|
||||
else
|
||||
TAG="feature"
|
||||
fi
|
||||
echo "tags=${IMAGE_NAME}:${TAG}-arm64" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.monitoring
|
||||
platforms: linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
combine-manifests:
|
||||
needs: [docker-amd, docker-arm]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Create and push manifests
|
||||
run: |
|
||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||
TAG="latest"
|
||||
|
||||
docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \
|
||||
${IMAGE_NAME}:${TAG}-amd64 \
|
||||
${IMAGE_NAME}:${TAG}-arm64
|
||||
|
||||
elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then
|
||||
TAG="canary"
|
||||
docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \
|
||||
${IMAGE_NAME}:${TAG}-amd64 \
|
||||
${IMAGE_NAME}:${TAG}-arm64
|
||||
|
||||
else
|
||||
TAG="feature"
|
||||
docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \
|
||||
${IMAGE_NAME}:${TAG}-amd64 \
|
||||
${IMAGE_NAME}:${TAG}-arm64
|
||||
fi
|
||||
6
.github/workflows/pull-request.yml
vendored
6
.github/workflows/pull-request.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.18.0
|
||||
node-version: 20.9.0
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run server:build
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.18.0
|
||||
node-version: 20.9.0
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run server:build
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.18.0
|
||||
node-version: 20.9.0
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run server:build
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -39,3 +39,6 @@ yarn-error.log*
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
|
||||
.db
|
||||
@@ -14,12 +14,10 @@ We have a few guidelines to follow when contributing to this project:
|
||||
|
||||
## Commit Convention
|
||||
|
||||
|
||||
Before you create a Pull Request, please make sure your commit message follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
|
||||
|
||||
### Commit Message Format
|
||||
|
||||
|
||||
```
|
||||
<type>[optional scope]: <description>
|
||||
|
||||
@@ -54,6 +52,8 @@ feat: add new feature
|
||||
|
||||
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
|
||||
|
||||
We use Node v20.9.0
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dokploy/dokploy.git
|
||||
cd dokploy
|
||||
@@ -73,9 +73,10 @@ Run the command that will spin up all the required services and files.
|
||||
pnpm run dokploy:setup
|
||||
```
|
||||
|
||||
Run this script
|
||||
Run this script
|
||||
|
||||
```bash
|
||||
pnpm run server:script
|
||||
pnpm run server:script
|
||||
```
|
||||
|
||||
Now run the development server.
|
||||
@@ -137,11 +138,18 @@ curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
||||
&& ./install.sh
|
||||
```
|
||||
|
||||
```bash
|
||||
# Install Railpack
|
||||
curl -sSL https://railpack.com/install.sh | sh
|
||||
```
|
||||
|
||||
```bash
|
||||
# Install Buildpacks
|
||||
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Pull Request
|
||||
|
||||
- The `main` branch is the source of truth and should always reflect the latest stable release.
|
||||
@@ -169,6 +177,7 @@ Let's take the example of `plausible` template.
|
||||
```typescript
|
||||
// EXAMPLE
|
||||
import {
|
||||
generateBase64,
|
||||
generateHash,
|
||||
generateRandomDomain,
|
||||
type Template,
|
||||
@@ -200,8 +209,8 @@ export function generate(schema: Schema): Template {
|
||||
|
||||
const mounts: Template["mounts"] = [
|
||||
{
|
||||
mountPath: "./clickhouse/clickhouse-config.xml",
|
||||
content: `some content......`,
|
||||
filePath: "./clickhouse/clickhouse-config.xml",
|
||||
content: "some content......",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -247,4 +256,3 @@ export function generate(schema: Schema): Template {
|
||||
## Docs & Website
|
||||
|
||||
To contribute to the Dokploy docs or website, please go to this [repository](https://github.com/Dokploy/website).
|
||||
|
||||
|
||||
10
Dockerfile
10
Dockerfile
@@ -1,4 +1,4 @@
|
||||
FROM node:18-slim AS base
|
||||
FROM node:20.9-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
@@ -7,7 +7,7 @@ FROM base AS build
|
||||
COPY . /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install dependencies
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
@@ -29,7 +29,7 @@ WORKDIR /app
|
||||
# Set production
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y curl unzip apache2-utils iproute2 && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy only the necessary files
|
||||
COPY --from=build /prod/dokploy/.next ./.next
|
||||
@@ -55,6 +55,10 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
||||
&& ./install.sh \
|
||||
&& pnpm install -g tsx
|
||||
|
||||
# Install Railpack
|
||||
ARG RAILPACK_VERSION=0.0.37
|
||||
RUN curl -sSL https://railpack.com/install.sh | bash
|
||||
|
||||
# Install buildpacks
|
||||
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:18-slim AS base
|
||||
FROM node:20.9-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
@@ -7,7 +7,7 @@ FROM base AS build
|
||||
COPY . /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install dependencies
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/dokploy install --frozen-lockfile
|
||||
|
||||
41
Dockerfile.monitoring
Normal file
41
Dockerfile.monitoring
Normal file
@@ -0,0 +1,41 @@
|
||||
# Build stage
|
||||
FROM golang:1.21-alpine3.19 AS builder
|
||||
|
||||
# Instalar dependencias necesarias
|
||||
RUN apk add --no-cache gcc musl-dev sqlite-dev
|
||||
|
||||
# Establecer el directorio de trabajo
|
||||
WORKDIR /app
|
||||
|
||||
# Copiar todo el código fuente primero
|
||||
COPY . .
|
||||
|
||||
# Movernos al directorio de la aplicación golang
|
||||
WORKDIR /app/apps/monitoring
|
||||
|
||||
# Descargar dependencias
|
||||
RUN go mod download
|
||||
|
||||
# Compilar la aplicación
|
||||
RUN CGO_ENABLED=1 GOOS=linux go build -o main main.go
|
||||
|
||||
# Etapa final
|
||||
FROM alpine:3.19
|
||||
|
||||
# Instalar SQLite y otras dependencias necesarias
|
||||
RUN apk add --no-cache sqlite-libs docker-cli
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copiar el binario compilado y el archivo monitor.go
|
||||
COPY --from=builder /app/apps/monitoring/main ./main
|
||||
COPY --from=builder /app/apps/monitoring/main.go ./monitor.go
|
||||
|
||||
# COPY --from=builder /app/apps/golang/.env ./.env
|
||||
|
||||
# Exponer el puerto
|
||||
ENV PORT=3001
|
||||
EXPOSE 3001
|
||||
|
||||
# Ejecutar la aplicación
|
||||
CMD ["./main"]
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:18-slim AS base
|
||||
FROM node:20.9-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
@@ -7,7 +7,7 @@ FROM base AS build
|
||||
COPY . /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install dependencies
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/schedules install --frozen-lockfile
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:18-slim AS base
|
||||
FROM node:20.9-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
@@ -7,7 +7,7 @@ FROM base AS build
|
||||
COPY . /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install dependencies
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/api install --frozen-lockfile
|
||||
|
||||
@@ -71,6 +71,11 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
<a href="https://mandarin3d.com/?ref=dokploy" target="_blank" style="display: inline-block;">
|
||||
<img src=".github/sponsors/mandarin.png" alt="Mandarin" height="50"/>
|
||||
</a>
|
||||
<a href="https://lightnode.com/?ref=dokploy" target="_blank" style="display: inline-block;">
|
||||
<img src=".github/sponsors/light-node.webp" alt="Lightnode" height="70"/>
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
### Premium Supporters 🥇
|
||||
@@ -89,8 +94,12 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
<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 style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"scripts": {
|
||||
"dev": "PORT=4000 tsx watch src/index.ts",
|
||||
"build": "tsc --project tsconfig.json",
|
||||
"start": "node --experimental-specifier-resolution=node dist/index.js",
|
||||
"start": "node dist/index.js",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -4,9 +4,9 @@ import "dotenv/config";
|
||||
import { zValidator } from "@hono/zod-validator";
|
||||
import { Queue } from "@nerimity/mimiqueue";
|
||||
import { createClient } from "redis";
|
||||
import { logger } from "./logger";
|
||||
import { type DeployJob, deployJobSchema } from "./schema";
|
||||
import { deploy } from "./utils";
|
||||
import { logger } from "./logger.js";
|
||||
import { type DeployJob, deployJobSchema } from "./schema.js";
|
||||
import { deploy } from "./utils.js";
|
||||
|
||||
const app = new Hono();
|
||||
const redisClient = createClient({
|
||||
@@ -28,7 +28,7 @@ app.use(async (c, next) => {
|
||||
|
||||
app.post("/deploy", zValidator("json", deployJobSchema), (c) => {
|
||||
const data = c.req.valid("json");
|
||||
const res = queue.add(data, { groupName: data.serverId });
|
||||
queue.add(data, { groupName: data.serverId });
|
||||
return c.json(
|
||||
{
|
||||
message: "Deployment Added",
|
||||
|
||||
@@ -64,7 +64,7 @@ export const deploy = async (job: DeployJob) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (_) {
|
||||
if (job.applicationType === "application") {
|
||||
await updateApplicationStatus(job.applicationId, "error");
|
||||
} else if (job.applicationType === "compose") {
|
||||
|
||||
@@ -1 +1 @@
|
||||
18.18.0
|
||||
20.9.0
|
||||
@@ -1,5 +1,5 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToAllConfigs, addSuffixToConfigsRoot } from "@dokploy/server";
|
||||
import { addSuffixToAllConfigs } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
@@ -293,29 +293,6 @@ networks:
|
||||
dokploy-network:
|
||||
`;
|
||||
|
||||
const expectedComposeFile7 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
networks:
|
||||
- dokploy-network
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
driver: bridge
|
||||
driver_opts:
|
||||
com.docker.network.driver.mtu: 1200
|
||||
|
||||
backend:
|
||||
driver: bridge
|
||||
attachable: true
|
||||
|
||||
external_network:
|
||||
external: true
|
||||
name: dokploy-network
|
||||
`;
|
||||
test("It shoudn't add suffix to dokploy-network", () => {
|
||||
const composeData = load(composeFile7) as ComposeSpecification;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToSecretsRoot } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { dump, load } from "js-yaml";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import {
|
||||
addSuffixToAllVolumes,
|
||||
addSuffixToVolumesInServices,
|
||||
} from "@dokploy/server";
|
||||
import { addSuffixToAllVolumes } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
98
apps/dokploy/__test__/deploy/github.test.ts
Normal file
98
apps/dokploy/__test__/deploy/github.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("GitHub Webhook Skip CI", () => {
|
||||
const mockGithubHeaders = {
|
||||
"x-github-event": "push",
|
||||
};
|
||||
|
||||
const createMockBody = (message: string) => ({
|
||||
head_commit: {
|
||||
message,
|
||||
},
|
||||
});
|
||||
|
||||
const skipKeywords = [
|
||||
"[skip ci]",
|
||||
"[ci skip]",
|
||||
"[no ci]",
|
||||
"[skip actions]",
|
||||
"[actions skip]",
|
||||
];
|
||||
|
||||
it("should detect skip keywords in commit message", () => {
|
||||
for (const keyword of skipKeywords) {
|
||||
const message = `feat: add new feature ${keyword}`;
|
||||
const commitMessage = extractCommitMessage(
|
||||
mockGithubHeaders,
|
||||
createMockBody(message),
|
||||
);
|
||||
expect(commitMessage.includes(keyword)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("should not detect skip keywords in normal commit message", () => {
|
||||
const message = "feat: add new feature";
|
||||
const commitMessage = extractCommitMessage(
|
||||
mockGithubHeaders,
|
||||
createMockBody(message),
|
||||
);
|
||||
for (const keyword of skipKeywords) {
|
||||
expect(commitMessage.includes(keyword)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle different webhook sources", () => {
|
||||
// GitHub
|
||||
expect(
|
||||
extractCommitMessage(
|
||||
{ "x-github-event": "push" },
|
||||
{ head_commit: { message: "[skip ci] test" } },
|
||||
),
|
||||
).toBe("[skip ci] test");
|
||||
|
||||
// GitLab
|
||||
expect(
|
||||
extractCommitMessage(
|
||||
{ "x-gitlab-event": "push" },
|
||||
{ commits: [{ message: "[skip ci] test" }] },
|
||||
),
|
||||
).toBe("[skip ci] test");
|
||||
|
||||
// Bitbucket
|
||||
expect(
|
||||
extractCommitMessage(
|
||||
{ "x-event-key": "repo:push" },
|
||||
{
|
||||
push: {
|
||||
changes: [{ new: { target: { message: "[skip ci] test" } } }],
|
||||
},
|
||||
},
|
||||
),
|
||||
).toBe("[skip ci] test");
|
||||
|
||||
// Gitea
|
||||
expect(
|
||||
extractCommitMessage(
|
||||
{ "x-gitea-event": "push" },
|
||||
{ commits: [{ message: "[skip ci] test" }] },
|
||||
),
|
||||
).toBe("[skip ci] test");
|
||||
});
|
||||
|
||||
it("should handle missing commit message", () => {
|
||||
expect(extractCommitMessage(mockGithubHeaders, {})).toBe("NEW COMMIT");
|
||||
expect(extractCommitMessage({ "x-gitlab-event": "push" }, {})).toBe(
|
||||
"NEW COMMIT",
|
||||
);
|
||||
expect(
|
||||
extractCommitMessage(
|
||||
{ "x-event-key": "repo:push" },
|
||||
{ push: { changes: [] } },
|
||||
),
|
||||
).toBe("NEW COMMIT");
|
||||
expect(extractCommitMessage({ "x-gitea-event": "push" }, {})).toBe(
|
||||
"NEW COMMIT",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -45,7 +45,7 @@ const baseApp: ApplicationNested = {
|
||||
previewWildcard: "",
|
||||
project: {
|
||||
env: "",
|
||||
adminId: "",
|
||||
organizationId: "",
|
||||
name: "",
|
||||
description: "",
|
||||
createdAt: "",
|
||||
|
||||
@@ -5,7 +5,7 @@ vi.mock("node:fs", () => ({
|
||||
default: fs,
|
||||
}));
|
||||
|
||||
import type { Admin, FileConfig } from "@dokploy/server";
|
||||
import type { FileConfig, User } from "@dokploy/server";
|
||||
import {
|
||||
createDefaultServerTraefikConfig,
|
||||
loadOrCreateConfig,
|
||||
@@ -13,20 +13,57 @@ import {
|
||||
} from "@dokploy/server";
|
||||
import { beforeEach, expect, test, vi } from "vitest";
|
||||
|
||||
const baseAdmin: Admin = {
|
||||
createdAt: "",
|
||||
authId: "",
|
||||
adminId: "string",
|
||||
const baseAdmin: User = {
|
||||
enablePaidFeatures: false,
|
||||
metricsConfig: {
|
||||
containers: {
|
||||
refreshRate: 20,
|
||||
services: {
|
||||
include: [],
|
||||
exclude: [],
|
||||
},
|
||||
},
|
||||
server: {
|
||||
type: "Dokploy",
|
||||
cronJob: "",
|
||||
port: 4500,
|
||||
refreshRate: 20,
|
||||
retentionDays: 2,
|
||||
token: "",
|
||||
thresholds: {
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
},
|
||||
urlCallback: "",
|
||||
},
|
||||
},
|
||||
cleanupCacheApplications: false,
|
||||
cleanupCacheOnCompose: false,
|
||||
cleanupCacheOnPreviews: false,
|
||||
createdAt: new Date(),
|
||||
serverIp: null,
|
||||
certificateType: "none",
|
||||
host: null,
|
||||
letsEncryptEmail: null,
|
||||
sshPrivateKey: null,
|
||||
enableDockerCleanup: false,
|
||||
enableLogRotation: false,
|
||||
logCleanupCron: null,
|
||||
serversQuantity: 0,
|
||||
stripeCustomerId: "",
|
||||
stripeSubscriptionId: "",
|
||||
banExpires: new Date(),
|
||||
banned: true,
|
||||
banReason: "",
|
||||
email: "",
|
||||
expirationDate: "",
|
||||
id: "",
|
||||
isRegistered: false,
|
||||
name: "",
|
||||
createdAt2: new Date().toISOString(),
|
||||
emailVerified: false,
|
||||
image: "",
|
||||
updatedAt: new Date(),
|
||||
twoFactorEnabled: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -77,8 +114,6 @@ test("Should not touch config without host", () => {
|
||||
});
|
||||
|
||||
test("Should remove websecure if https rollback to http", () => {
|
||||
const originalConfig: FileConfig = loadOrCreateConfig("dokploy");
|
||||
|
||||
updateServerTraefik(
|
||||
{ ...baseAdmin, certificateType: "letsencrypt" },
|
||||
"example.com",
|
||||
|
||||
@@ -26,7 +26,7 @@ const baseApp: ApplicationNested = {
|
||||
previewWildcard: "",
|
||||
project: {
|
||||
env: "",
|
||||
adminId: "",
|
||||
organizationId: "",
|
||||
name: "",
|
||||
description: "",
|
||||
createdAt: "",
|
||||
|
||||
@@ -13,6 +13,7 @@ export default defineConfig({
|
||||
NODE: "test",
|
||||
},
|
||||
},
|
||||
plugins: [tsconfigPaths()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@dokploy/server": path.resolve(
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
|
||||
import { CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
} from "@/components/ui/input-otp";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const Login2FASchema = z.object({
|
||||
pin: z.string().min(6, {
|
||||
message: "Pin is required",
|
||||
}),
|
||||
});
|
||||
|
||||
type Login2FA = z.infer<typeof Login2FASchema>;
|
||||
|
||||
interface Props {
|
||||
authId: string;
|
||||
}
|
||||
|
||||
export const Login2FA = ({ authId }: Props) => {
|
||||
const { push } = useRouter();
|
||||
|
||||
const { mutateAsync, isLoading, isError, error } =
|
||||
api.auth.verifyLogin2FA.useMutation();
|
||||
|
||||
const form = useForm<Login2FA>({
|
||||
defaultValues: {
|
||||
pin: "",
|
||||
},
|
||||
resolver: zodResolver(Login2FASchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
pin: "",
|
||||
});
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
|
||||
const onSubmit = async (data: Login2FA) => {
|
||||
await mutateAsync({
|
||||
pin: data.pin,
|
||||
id: authId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Signin successfully", {
|
||||
duration: 2000,
|
||||
});
|
||||
|
||||
push("/dashboard/projects");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Signin failed", {
|
||||
duration: 2000,
|
||||
});
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
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>
|
||||
)}
|
||||
<CardTitle className="text-xl font-bold">2FA Login</CardTitle>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="pin"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col justify-center max-sm:items-center">
|
||||
<FormLabel>Pin</FormLabel>
|
||||
<FormControl>
|
||||
<InputOTP maxLength={6} {...field}>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Please enter the 6 digits code provided by your authenticator
|
||||
app.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button isLoading={isLoading} type="submit">
|
||||
Submit 2FA
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -130,7 +130,7 @@ const createStringToJSONSchema = (schema: z.ZodTypeAny) => {
|
||||
}
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
ctx.addIssue({ code: "custom", message: "Invalid JSON format" });
|
||||
return z.NEVER;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Server } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -81,7 +80,8 @@ export const AddCommand = ({ applicationId }: Props) => {
|
||||
<div>
|
||||
<CardTitle className="text-xl">Run Command</CardTitle>
|
||||
<CardDescription>
|
||||
Run a custom command in the container
|
||||
Run a custom command in the container after the application
|
||||
initialized
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
portId: string;
|
||||
}
|
||||
|
||||
export const DeletePort = ({ portId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync, isLoading } = api.port.delete.useMutation();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the port
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
portId,
|
||||
})
|
||||
.then((data) => {
|
||||
utils.application.one.invalidate({
|
||||
applicationId: data?.applicationId,
|
||||
});
|
||||
|
||||
toast.success("Port delete successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting the port");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -45,18 +45,29 @@ type AddPort = z.infer<typeof AddPortSchema>;
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
portId?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AddPort = ({
|
||||
export const HandlePorts = ({
|
||||
applicationId,
|
||||
portId,
|
||||
children = <PlusIcon className="h-4 w-4" />,
|
||||
}: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.port.create.useMutation();
|
||||
const { data } = api.port.one.useQuery(
|
||||
{
|
||||
portId: portId ?? "",
|
||||
},
|
||||
{
|
||||
enabled: !!portId,
|
||||
},
|
||||
);
|
||||
const { mutateAsync, isLoading, error, isError } = portId
|
||||
? api.port.update.useMutation()
|
||||
: api.port.create.useMutation();
|
||||
|
||||
const form = useForm<AddPort>({
|
||||
defaultValues: {
|
||||
@@ -68,32 +79,46 @@ export const AddPort = ({
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
publishedPort: 0,
|
||||
targetPort: 0,
|
||||
publishedPort: data?.publishedPort ?? 0,
|
||||
targetPort: data?.targetPort ?? 0,
|
||||
protocol: data?.protocol ?? "tcp",
|
||||
});
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||
|
||||
const onSubmit = async (data: AddPort) => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
...data,
|
||||
portId: portId || "",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Port Created");
|
||||
toast.success(portId ? "Port Updated" : "Port Created");
|
||||
await utils.application.one.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error creating the port");
|
||||
toast.error(
|
||||
portId ? "Error updating the port" : "Error creating the port",
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>{children}</Button>
|
||||
{portId ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-blue-500/10 "
|
||||
>
|
||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button>{children}</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
@@ -204,7 +229,7 @@ export const AddPort = ({
|
||||
form="hook-form-add-port"
|
||||
type="submit"
|
||||
>
|
||||
Create
|
||||
{portId ? "Update" : "Create"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
@@ -1,4 +1,6 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -7,23 +9,24 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { Rss } from "lucide-react";
|
||||
import React from "react";
|
||||
import { AddPort } from "./add-port";
|
||||
import { DeletePort } from "./delete-port";
|
||||
import { UpdatePort } from "./update-port";
|
||||
import { Rss, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { HandlePorts } from "./handle-ports";
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const ShowPorts = ({ applicationId }: Props) => {
|
||||
const { data } = api.application.one.useQuery(
|
||||
const { data, refetch } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
|
||||
const { mutateAsync: deletePort, isLoading: isRemoving } =
|
||||
api.port.delete.useMutation();
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||
@@ -35,7 +38,7 @@ export const ShowPorts = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
|
||||
{data && data?.ports.length > 0 && (
|
||||
<AddPort applicationId={applicationId}>Add Port</AddPort>
|
||||
<HandlePorts applicationId={applicationId}>Add Port</HandlePorts>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
@@ -45,7 +48,7 @@ export const ShowPorts = ({ applicationId }: Props) => {
|
||||
<span className="text-base text-muted-foreground">
|
||||
No ports configured
|
||||
</span>
|
||||
<AddPort applicationId={applicationId}>Add Port</AddPort>
|
||||
<HandlePorts applicationId={applicationId}>Add Port</HandlePorts>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col pt-2 gap-4">
|
||||
@@ -78,8 +81,36 @@ export const ShowPorts = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-4">
|
||||
<UpdatePort portId={port.portId} />
|
||||
<DeletePort portId={port.portId} />
|
||||
<HandlePorts
|
||||
applicationId={applicationId}
|
||||
portId={port.portId}
|
||||
/>
|
||||
<DialogAction
|
||||
title="Delete Port"
|
||||
description="Are you sure you want to delete this port?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await deletePort({
|
||||
portId: port.portId,
|
||||
})
|
||||
.then(() => {
|
||||
refetch();
|
||||
toast.success("Port deleted successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting port");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10 "
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input, NumberInput } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const UpdatePortSchema = z.object({
|
||||
publishedPort: z.number().int().min(1).max(65535),
|
||||
targetPort: z.number().int().min(1).max(65535),
|
||||
protocol: z.enum(["tcp", "udp"], {
|
||||
required_error: "Protocol is required",
|
||||
invalid_type_error: "Protocol must be a valid protocol",
|
||||
}),
|
||||
});
|
||||
|
||||
type UpdatePort = z.infer<typeof UpdatePortSchema>;
|
||||
|
||||
interface Props {
|
||||
portId: string;
|
||||
}
|
||||
|
||||
export const UpdatePort = ({ portId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const { data } = api.port.one.useQuery(
|
||||
{
|
||||
portId,
|
||||
},
|
||||
{
|
||||
enabled: !!portId,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.port.update.useMutation();
|
||||
|
||||
const form = useForm<UpdatePort>({
|
||||
defaultValues: {},
|
||||
resolver: zodResolver(UpdatePortSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
publishedPort: data.publishedPort,
|
||||
targetPort: data.targetPort,
|
||||
protocol: data.protocol,
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
const onSubmit = async (data: UpdatePort) => {
|
||||
await mutateAsync({
|
||||
portId,
|
||||
publishedPort: data.publishedPort,
|
||||
targetPort: data.targetPort,
|
||||
protocol: data.protocol,
|
||||
})
|
||||
.then(async (response) => {
|
||||
toast.success("Port Updated");
|
||||
await utils.application.one.invalidate({
|
||||
applicationId: response?.applicationId,
|
||||
});
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating the port");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update</DialogTitle>
|
||||
<DialogDescription>Update the port</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-update-redirect"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="publishedPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Published Port</FormLabel>
|
||||
<FormControl>
|
||||
<NumberInput placeholder="1-65535" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="targetPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Target Port</FormLabel>
|
||||
<FormControl>
|
||||
<NumberInput placeholder="1-65535" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="protocol"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className="md:col-span-2">
|
||||
<FormLabel>Protocol</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a protocol" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent defaultValue={"none"}>
|
||||
<SelectItem value={"none"} disabled>
|
||||
None
|
||||
</SelectItem>
|
||||
<SelectItem value={"tcp"}>TCP</SelectItem>
|
||||
<SelectItem value={"udp"}>UDP</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-update-redirect"
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
redirectId: string;
|
||||
}
|
||||
|
||||
export const DeleteRedirect = ({ redirectId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync, isLoading } = api.redirects.delete.useMutation();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
redirect
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
redirectId,
|
||||
})
|
||||
.then((data) => {
|
||||
utils.application.one.invalidate({
|
||||
applicationId: data?.applicationId,
|
||||
});
|
||||
utils.application.readTraefikConfig.invalidate({
|
||||
applicationId: data?.applicationId,
|
||||
});
|
||||
toast.success("Redirect delete successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting the redirect");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -31,7 +31,7 @@ import { Separator } from "@/components/ui/separator";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -77,19 +77,32 @@ const redirectPresets = [
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
redirectId?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AddRedirect = ({
|
||||
export const HandleRedirect = ({
|
||||
applicationId,
|
||||
redirectId,
|
||||
children = <PlusIcon className="w-4 h-4" />,
|
||||
}: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [presetSelected, setPresetSelected] = useState("");
|
||||
|
||||
const { data, refetch } = api.redirects.one.useQuery(
|
||||
{
|
||||
redirectId: redirectId || "",
|
||||
},
|
||||
{
|
||||
enabled: !!redirectId,
|
||||
},
|
||||
);
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.redirects.create.useMutation();
|
||||
const { mutateAsync, isLoading, error, isError } = redirectId
|
||||
? api.redirects.update.useMutation()
|
||||
: api.redirects.create.useMutation();
|
||||
|
||||
const form = useForm<AddRedirect>({
|
||||
defaultValues: {
|
||||
@@ -102,29 +115,35 @@ export const AddRedirect = ({
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
permanent: false,
|
||||
regex: "",
|
||||
replacement: "",
|
||||
permanent: data?.permanent || false,
|
||||
regex: data?.regex || "",
|
||||
replacement: data?.replacement || "",
|
||||
});
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||
|
||||
const onSubmit = async (data: AddRedirect) => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
...data,
|
||||
redirectId: redirectId || "",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Redirect Created");
|
||||
toast.success(redirectId ? "Redirect Updated" : "Redirect Created");
|
||||
await utils.application.one.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
refetch();
|
||||
await utils.application.readTraefikConfig.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
onDialogToggle(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error creating the redirect");
|
||||
toast.error(
|
||||
redirectId
|
||||
? "Error updating the redirect"
|
||||
: "Error creating the redirect",
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -148,7 +167,17 @@ export const AddRedirect = ({
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onDialogToggle}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>{children}</Button>
|
||||
{redirectId ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-blue-500/10 "
|
||||
>
|
||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button>{children}</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
@@ -243,7 +272,7 @@ export const AddRedirect = ({
|
||||
form="hook-form-add-redirect"
|
||||
type="submit"
|
||||
>
|
||||
Create
|
||||
{redirectId ? "Update" : "Create"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
@@ -1,3 +1,5 @@
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -6,23 +8,27 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { Split } from "lucide-react";
|
||||
import React from "react";
|
||||
import { AddRedirect } from "./add-redirect";
|
||||
import { DeleteRedirect } from "./delete-redirect";
|
||||
import { UpdateRedirect } from "./update-redirect";
|
||||
import { Split, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { HandleRedirect } from "./handle-redirect";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const ShowRedirects = ({ applicationId }: Props) => {
|
||||
const { data } = api.application.one.useQuery(
|
||||
const { data, refetch } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
|
||||
const { mutateAsync: deleteRedirect, isLoading: isRemoving } =
|
||||
api.redirects.delete.useMutation();
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||
@@ -35,7 +41,9 @@ export const ShowRedirects = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
|
||||
{data && data?.redirects.length > 0 && (
|
||||
<AddRedirect applicationId={applicationId}>Add Redirect</AddRedirect>
|
||||
<HandleRedirect applicationId={applicationId}>
|
||||
Add Redirect
|
||||
</HandleRedirect>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
@@ -45,9 +53,9 @@ export const ShowRedirects = ({ applicationId }: Props) => {
|
||||
<span className="text-base text-muted-foreground">
|
||||
No redirects configured
|
||||
</span>
|
||||
<AddRedirect applicationId={applicationId}>
|
||||
<HandleRedirect applicationId={applicationId}>
|
||||
Add Redirect
|
||||
</AddRedirect>
|
||||
</HandleRedirect>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col pt-2">
|
||||
@@ -76,8 +84,40 @@ export const ShowRedirects = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-4">
|
||||
<UpdateRedirect redirectId={redirect.redirectId} />
|
||||
<DeleteRedirect redirectId={redirect.redirectId} />
|
||||
<HandleRedirect
|
||||
redirectId={redirect.redirectId}
|
||||
applicationId={applicationId}
|
||||
/>
|
||||
|
||||
<DialogAction
|
||||
title="Delete Redirect"
|
||||
description="Are you sure you want to delete this redirect?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await deleteRedirect({
|
||||
redirectId: redirect.redirectId,
|
||||
})
|
||||
.then(() => {
|
||||
refetch();
|
||||
utils.application.readTraefikConfig.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
toast.success("Redirect deleted successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting redirect");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10"
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
const UpdateRedirectSchema = z.object({
|
||||
regex: z.string().min(1, "Regex required"),
|
||||
permanent: z.boolean().default(false),
|
||||
replacement: z.string().min(1, "Replacement required"),
|
||||
});
|
||||
|
||||
type UpdateRedirect = z.infer<typeof UpdateRedirectSchema>;
|
||||
|
||||
interface Props {
|
||||
redirectId: string;
|
||||
}
|
||||
|
||||
export const UpdateRedirect = ({ redirectId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { data } = api.redirects.one.useQuery(
|
||||
{
|
||||
redirectId,
|
||||
},
|
||||
{
|
||||
enabled: !!redirectId,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.redirects.update.useMutation();
|
||||
|
||||
const form = useForm<UpdateRedirect>({
|
||||
defaultValues: {
|
||||
permanent: false,
|
||||
regex: "",
|
||||
replacement: "",
|
||||
},
|
||||
resolver: zodResolver(UpdateRedirectSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
permanent: data.permanent || false,
|
||||
regex: data.regex || "",
|
||||
replacement: data.replacement || "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
const onSubmit = async (data: UpdateRedirect) => {
|
||||
await mutateAsync({
|
||||
redirectId,
|
||||
permanent: data.permanent,
|
||||
regex: data.regex,
|
||||
replacement: data.replacement,
|
||||
})
|
||||
.then(async (response) => {
|
||||
toast.success("Redirect Updated");
|
||||
await utils.application.one.invalidate({
|
||||
applicationId: response?.applicationId,
|
||||
});
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating the redirect");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update</DialogTitle>
|
||||
<DialogDescription>Update the redirect</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-update-redirect"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="regex"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Regex</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="^http://localhost/(.*)" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="replacement"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Replacement</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="http://mydomain/$${1}" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="permanent"
|
||||
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>Permanent</FormLabel>
|
||||
<FormDescription>
|
||||
Set the permanent option to true to apply a permanent
|
||||
redirection.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-update-redirect"
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
securityId: string;
|
||||
}
|
||||
|
||||
export const DeleteSecurity = ({ securityId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync, isLoading } = api.security.delete.useMutation();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
security
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
securityId,
|
||||
})
|
||||
.then((data) => {
|
||||
utils.application.one.invalidate({
|
||||
applicationId: data?.applicationId,
|
||||
});
|
||||
utils.application.readTraefikConfig.invalidate({
|
||||
applicationId: data?.applicationId,
|
||||
});
|
||||
toast.success("Security delete successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting the security");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -35,17 +35,29 @@ type AddSecurity = z.infer<typeof AddSecuritychema>;
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
securityId?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AddSecurity = ({
|
||||
export const HandleSecurity = ({
|
||||
applicationId,
|
||||
securityId,
|
||||
children = <PlusIcon className="h-4 w-4" />,
|
||||
}: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.security.create.useMutation();
|
||||
const { data } = api.security.one.useQuery(
|
||||
{
|
||||
securityId: securityId ?? "",
|
||||
},
|
||||
{
|
||||
enabled: !!securityId,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } = securityId
|
||||
? api.security.update.useMutation()
|
||||
: api.security.create.useMutation();
|
||||
|
||||
const form = useForm<AddSecurity>({
|
||||
defaultValues: {
|
||||
@@ -56,16 +68,20 @@ export const AddSecurity = ({
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
form.reset({
|
||||
username: data?.username || "",
|
||||
password: data?.password || "",
|
||||
});
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||
|
||||
const onSubmit = async (data: AddSecurity) => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
...data,
|
||||
securityId: securityId || "",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Security Created");
|
||||
toast.success(securityId ? "Security Updated" : "Security Created");
|
||||
await utils.application.one.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
@@ -75,20 +91,34 @@ export const AddSecurity = ({
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error creating security");
|
||||
toast.error(
|
||||
securityId
|
||||
? "Error updating the security"
|
||||
: "Error creating security",
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>{children}</Button>
|
||||
{securityId ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-blue-500/10 "
|
||||
>
|
||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button>{children}</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Security</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add security to your application
|
||||
{securityId ? "Update" : "Add"} security to your application
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
@@ -137,7 +167,7 @@ export const AddSecurity = ({
|
||||
form="hook-form-add-security"
|
||||
type="submit"
|
||||
>
|
||||
Create
|
||||
{securityId ? "Update" : "Create"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
@@ -1,3 +1,5 @@
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -6,23 +8,26 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { LockKeyhole } from "lucide-react";
|
||||
import React from "react";
|
||||
import { AddSecurity } from "./add-security";
|
||||
import { DeleteSecurity } from "./delete-security";
|
||||
import { UpdateSecurity } from "./update-security";
|
||||
import { LockKeyhole, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { HandleSecurity } from "./handle-security";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const ShowSecurity = ({ applicationId }: Props) => {
|
||||
const { data } = api.application.one.useQuery(
|
||||
const { data, refetch } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
|
||||
const { mutateAsync: deleteSecurity, isLoading: isRemoving } =
|
||||
api.security.delete.useMutation();
|
||||
|
||||
const utils = api.useUtils();
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||
@@ -32,7 +37,9 @@ export const ShowSecurity = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
|
||||
{data && data?.security.length > 0 && (
|
||||
<AddSecurity applicationId={applicationId}>Add Security</AddSecurity>
|
||||
<HandleSecurity applicationId={applicationId}>
|
||||
Add Security
|
||||
</HandleSecurity>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
@@ -42,9 +49,9 @@ export const ShowSecurity = ({ applicationId }: Props) => {
|
||||
<span className="text-base text-muted-foreground">
|
||||
No security configured
|
||||
</span>
|
||||
<AddSecurity applicationId={applicationId}>
|
||||
<HandleSecurity applicationId={applicationId}>
|
||||
Add Security
|
||||
</AddSecurity>
|
||||
</HandleSecurity>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col pt-2">
|
||||
@@ -67,8 +74,39 @@ export const ShowSecurity = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateSecurity securityId={security.securityId} />
|
||||
<DeleteSecurity securityId={security.securityId} />
|
||||
<HandleSecurity
|
||||
securityId={security.securityId}
|
||||
applicationId={applicationId}
|
||||
/>
|
||||
<DialogAction
|
||||
title="Delete Security"
|
||||
description="Are you sure you want to delete this security?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await deleteSecurity({
|
||||
securityId: security.securityId,
|
||||
})
|
||||
.then(() => {
|
||||
refetch();
|
||||
utils.application.readTraefikConfig.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
toast.success("Security deleted successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting security");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10"
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const UpdateSecuritySchema = z.object({
|
||||
username: z.string().min(1, "Username is required"),
|
||||
password: z.string().min(1, "Password is required"),
|
||||
});
|
||||
|
||||
type UpdateSecurity = z.infer<typeof UpdateSecuritySchema>;
|
||||
|
||||
interface Props {
|
||||
securityId: string;
|
||||
}
|
||||
|
||||
export const UpdateSecurity = ({ securityId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const { data } = api.security.one.useQuery(
|
||||
{
|
||||
securityId,
|
||||
},
|
||||
{
|
||||
enabled: !!securityId,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.security.update.useMutation();
|
||||
|
||||
const form = useForm<UpdateSecurity>({
|
||||
defaultValues: {
|
||||
username: "",
|
||||
password: "",
|
||||
},
|
||||
resolver: zodResolver(UpdateSecuritySchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
username: data.username || "",
|
||||
password: data.password || "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
const onSubmit = async (data: UpdateSecurity) => {
|
||||
await mutateAsync({
|
||||
securityId,
|
||||
username: data.username,
|
||||
password: data.password,
|
||||
})
|
||||
.then(async (response) => {
|
||||
toast.success("Security Updated");
|
||||
await utils.application.one.invalidate({
|
||||
applicationId: response?.applicationId,
|
||||
});
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating the security");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update</DialogTitle>
|
||||
<DialogDescription>Update the security</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-update-security"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 "
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="test1" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="test" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-update-security"
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,297 +0,0 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import React, { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
|
||||
const addResourcesApplication = z.object({
|
||||
memoryReservation: z.number().nullable().optional(),
|
||||
cpuLimit: z.number().nullable().optional(),
|
||||
memoryLimit: z.number().nullable().optional(),
|
||||
cpuReservation: z.number().nullable().optional(),
|
||||
});
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
type AddResourcesApplication = z.infer<typeof addResourcesApplication>;
|
||||
|
||||
export const ShowApplicationResources = ({ applicationId }: Props) => {
|
||||
const { data, refetch } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
const { mutateAsync, isLoading } = api.application.update.useMutation();
|
||||
const form = useForm<AddResourcesApplication>({
|
||||
defaultValues: {},
|
||||
resolver: zodResolver(addResourcesApplication),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
cpuLimit: data?.cpuLimit || undefined,
|
||||
cpuReservation: data?.cpuReservation || undefined,
|
||||
memoryLimit: data?.memoryLimit || undefined,
|
||||
memoryReservation: data?.memoryReservation || undefined,
|
||||
});
|
||||
}
|
||||
}, [data, form, form.reset]);
|
||||
|
||||
const onSubmit = async (formData: AddResourcesApplication) => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
cpuLimit: formData.cpuLimit || null,
|
||||
cpuReservation: formData.cpuReservation || null,
|
||||
memoryLimit: formData.memoryLimit || null,
|
||||
memoryReservation: formData.memoryReservation || null,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Resources Updated");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating the resources");
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Resources</CardTitle>
|
||||
<CardDescription>
|
||||
If you want to decrease or increase the resources to a specific.
|
||||
application or database
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<AlertBlock type="info">
|
||||
Please remember to click Redeploy after modify the resources to apply
|
||||
the changes.
|
||||
</AlertBlock>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-8 "
|
||||
>
|
||||
<div className="grid w-full md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="memoryReservation"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>Memory Reservation</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Memory soft limit in bytes. Example: 256MB =
|
||||
268435456 bytes
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="268435456 (256MB in bytes)"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="memoryLimit"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>Memory Limit</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Memory hard limit in bytes. Example: 1GB =
|
||||
1073741824 bytes
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="1073741824 (1GB in bytes)"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cpuLimit"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>CPU Limit</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
CPU quota in units of 10^-9 CPUs. Example: 2
|
||||
CPUs = 2000000000
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="2000000000 (2 CPUs)"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cpuReservation"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>CPU Reservation</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
CPU shares (relative weight). Example: 1 CPU =
|
||||
1000000000
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="1000000000 (1 CPU)"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full justify-end">
|
||||
<Button isLoading={isLoading} type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -16,42 +16,78 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
Tooltip,
|
||||
} 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 addResourcesMongo = z.object({
|
||||
memoryReservation: z.number().nullable().optional(),
|
||||
cpuLimit: z.number().nullable().optional(),
|
||||
memoryLimit: z.number().nullable().optional(),
|
||||
cpuReservation: z.number().nullable().optional(),
|
||||
const addResourcesSchema = z.object({
|
||||
memoryReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
cpuReservation: z.string().optional(),
|
||||
});
|
||||
|
||||
export type ServiceType =
|
||||
| "postgres"
|
||||
| "mongo"
|
||||
| "redis"
|
||||
| "mysql"
|
||||
| "mariadb"
|
||||
| "application";
|
||||
|
||||
interface Props {
|
||||
mongoId: string;
|
||||
id: string;
|
||||
type: ServiceType | "application";
|
||||
}
|
||||
|
||||
type AddResourcesMongo = z.infer<typeof addResourcesMongo>;
|
||||
export const ShowMongoResources = ({ mongoId }: Props) => {
|
||||
const { data, refetch } = api.mongo.one.useQuery(
|
||||
{
|
||||
mongoId,
|
||||
type AddResources = z.infer<typeof addResourcesSchema>;
|
||||
export const ShowResources = ({ 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 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 } = mutationMap[type]
|
||||
? mutationMap[type]()
|
||||
: api.mongo.update.useMutation();
|
||||
|
||||
const form = useForm<AddResources>({
|
||||
defaultValues: {
|
||||
cpuLimit: "",
|
||||
cpuReservation: "",
|
||||
memoryLimit: "",
|
||||
memoryReservation: "",
|
||||
},
|
||||
{ enabled: !!mongoId },
|
||||
);
|
||||
const { mutateAsync, isLoading } = api.mongo.update.useMutation();
|
||||
const form = useForm<AddResourcesMongo>({
|
||||
defaultValues: {},
|
||||
resolver: zodResolver(addResourcesMongo),
|
||||
resolver: zodResolver(addResourcesSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -65,9 +101,14 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
|
||||
}
|
||||
}, [data, form, form.reset]);
|
||||
|
||||
const onSubmit = async (formData: AddResourcesMongo) => {
|
||||
const onSubmit = async (formData: AddResources) => {
|
||||
await mutateAsync({
|
||||
mongoId,
|
||||
mongoId: id || "",
|
||||
postgresId: id || "",
|
||||
redisId: id || "",
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
applicationId: id || "",
|
||||
cpuLimit: formData.cpuLimit || null,
|
||||
cpuReservation: formData.cpuReservation || null,
|
||||
memoryLimit: formData.memoryLimit || null,
|
||||
@@ -81,6 +122,7 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
|
||||
toast.error("Error updating the resources");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
@@ -102,50 +144,6 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
|
||||
className="grid w-full gap-8 "
|
||||
>
|
||||
<div className="grid w-full md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="memoryReservation"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>Memory Reservation</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Memory soft limit in bytes. Example: 256MB =
|
||||
268435456 bytes
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="268435456 (256MB in bytes)"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="memoryLimit"
|
||||
@@ -172,18 +170,6 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
|
||||
<Input
|
||||
placeholder="1073741824 (1GB in bytes)"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -191,6 +177,37 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="memoryReservation"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>Memory Reservation</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Memory soft limit in bytes. Example: 256MB =
|
||||
268435456 bytes
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="268435456 (256MB in bytes)"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -219,17 +236,6 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
|
||||
placeholder="2000000000 (2 CPUs)"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -260,22 +266,7 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="1000000000 (1 CPU)"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Input placeholder="1000000000 (1 CPU)" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { File, Loader2 } from "lucide-react";
|
||||
import React from "react";
|
||||
import { UpdateTraefikConfig } from "./update-traefik-config";
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
mountId: string;
|
||||
refetch: () => void;
|
||||
}
|
||||
export const DeleteVolume = ({ mountId, refetch }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.mounts.remove.useMutation();
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the mount
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
mountId,
|
||||
})
|
||||
.then(() => {
|
||||
refetch();
|
||||
toast.success("Mount deleted successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting the mount");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,6 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -7,40 +9,48 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { Package } from "lucide-react";
|
||||
import React from "react";
|
||||
import { Package, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import type { ServiceType } from "../show-resources";
|
||||
import { AddVolumes } from "./add-volumes";
|
||||
import { DeleteVolume } from "./delete-volume";
|
||||
import { UpdateVolume } from "./update-volume";
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
id: string;
|
||||
type: ServiceType | "compose";
|
||||
}
|
||||
|
||||
export const ShowVolumes = ({ applicationId }: Props) => {
|
||||
const { data, refetch } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
|
||||
export const ShowVolumes = ({ 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 }),
|
||||
compose: () =>
|
||||
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
|
||||
};
|
||||
const { data, refetch } = queryMap[type]
|
||||
? queryMap[type]()
|
||||
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||
const { mutateAsync: deleteVolume, isLoading: isRemoving } =
|
||||
api.mounts.remove.useMutation();
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||
<div>
|
||||
<CardTitle className="text-xl">Volumes</CardTitle>
|
||||
<CardDescription>
|
||||
If you want to persist data in this application use the following
|
||||
config to setup the volumes
|
||||
If you want to persist data in this service use the following config
|
||||
to setup the volumes
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
{data && data?.mounts.length > 0 && (
|
||||
<AddVolumes
|
||||
serviceId={applicationId}
|
||||
refetch={refetch}
|
||||
serviceType="application"
|
||||
>
|
||||
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
||||
Add Volume
|
||||
</AddVolumes>
|
||||
)}
|
||||
@@ -52,17 +62,13 @@ export const ShowVolumes = ({ applicationId }: Props) => {
|
||||
<span className="text-base text-muted-foreground">
|
||||
No volumes/mounts configured
|
||||
</span>
|
||||
<AddVolumes
|
||||
serviceId={applicationId}
|
||||
refetch={refetch}
|
||||
serviceType="application"
|
||||
>
|
||||
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
||||
Add Volume
|
||||
</AddVolumes>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col pt-2 gap-4">
|
||||
<AlertBlock type="info">
|
||||
<AlertBlock type="warning">
|
||||
Please remember to click Redeploy after adding, editing, or
|
||||
deleting a mount to apply the changes.
|
||||
</AlertBlock>
|
||||
@@ -73,7 +79,8 @@ export const ShowVolumes = ({ applicationId }: Props) => {
|
||||
key={mount.mountId}
|
||||
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 flex-col gap-4 sm:gap-8">
|
||||
{/* <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="flex flex-col gap-1">
|
||||
<span className="font-medium">Mount Type</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
@@ -90,21 +97,12 @@ export const ShowVolumes = ({ applicationId }: Props) => {
|
||||
)}
|
||||
|
||||
{mount.type === "file" && (
|
||||
<>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium">Content</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{mount.content}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<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">Content</span>
|
||||
<span className="text-sm text-muted-foreground line-clamp-[10] whitespace-break-spaces">
|
||||
{mount.content}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{mount.type === "bind" && (
|
||||
<div className="flex flex-col gap-1">
|
||||
@@ -114,21 +112,55 @@ export const ShowVolumes = ({ applicationId }: Props) => {
|
||||
</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>
|
||||
{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>
|
||||
<div className="flex flex-row gap-1">
|
||||
<UpdateVolume
|
||||
mountId={mount.mountId}
|
||||
type={mount.type}
|
||||
refetch={refetch}
|
||||
serviceType="application"
|
||||
serviceType={type}
|
||||
/>
|
||||
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
||||
<DialogAction
|
||||
title="Delete Volume"
|
||||
description="Are you sure you want to delete this volume?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await deleteVolume({
|
||||
mountId: mount.mountId,
|
||||
})
|
||||
.then(() => {
|
||||
refetch();
|
||||
toast.success("Volume deleted successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting volume");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10"
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Pencil } from "lucide-react";
|
||||
import { PenBoxIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -77,7 +77,7 @@ export const UpdateVolume = ({
|
||||
serviceType,
|
||||
}: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const _utils = api.useUtils();
|
||||
const { data } = api.mounts.one.useQuery(
|
||||
{
|
||||
mountId,
|
||||
@@ -177,8 +177,13 @@ export const UpdateVolume = ({
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<Pencil className="size-4 text-muted-foreground" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-blue-500/10 "
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<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">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
@@ -25,6 +26,7 @@ enum BuildType {
|
||||
paketo_buildpacks = "paketo_buildpacks",
|
||||
nixpacks = "nixpacks",
|
||||
static = "static",
|
||||
railpack = "railpack",
|
||||
}
|
||||
|
||||
const mySchema = z.discriminatedUnion("buildType", [
|
||||
@@ -53,6 +55,9 @@ const mySchema = z.discriminatedUnion("buildType", [
|
||||
z.object({
|
||||
buildType: z.literal("static"),
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal("railpack"),
|
||||
}),
|
||||
]);
|
||||
|
||||
type AddTemplate = z.infer<typeof mySchema>;
|
||||
@@ -173,6 +178,15 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||
Dockerfile
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="railpack" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
Railpack{" "}
|
||||
<Badge className="ml-1 text-xs px-1">New</Badge>
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="nixpacks" />
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Copy, TrashIcon } 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 deleteApplicationSchema = z.object({
|
||||
projectName: z.string().min(1, {
|
||||
message: "Application name is required",
|
||||
}),
|
||||
});
|
||||
|
||||
type DeleteApplication = z.infer<typeof deleteApplicationSchema>;
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const DeleteApplication = ({ applicationId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync, isLoading } = api.application.delete.useMutation();
|
||||
const { data } = api.application.one.useQuery(
|
||||
{ applicationId },
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
const { push } = useRouter();
|
||||
const form = useForm<DeleteApplication>({
|
||||
defaultValues: {
|
||||
projectName: "",
|
||||
},
|
||||
resolver: zodResolver(deleteApplicationSchema),
|
||||
});
|
||||
|
||||
const onSubmit = async (formData: DeleteApplication) => {
|
||||
const expectedName = `${data?.name}/${data?.appName}`;
|
||||
if (formData.projectName === expectedName) {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Application deleted successfully");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting the application");
|
||||
});
|
||||
} else {
|
||||
form.setError("projectName", {
|
||||
message: "Project name does not match",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||
<DialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
application. If you are sure please enter the application name to
|
||||
delete this application.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
id="hook-form-delete-application"
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="projectName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center gap-2">
|
||||
<span>
|
||||
To confirm, type{" "}
|
||||
<Badge
|
||||
className="p-2 rounded-md ml-1 mr-1 hover:border-primary hover:text-primary-foreground hover:bg-primary hover:cursor-pointer"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (data?.name && data?.appName) {
|
||||
navigator.clipboard.writeText(
|
||||
`${data.name}/${data.appName}`,
|
||||
);
|
||||
toast.success("Copied to clipboard. Be careful!");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{data?.name}/{data?.appName}
|
||||
<Copy className="h-4 w-4 ml-1 text-muted-foreground" />
|
||||
</Badge>{" "}
|
||||
in the box below:
|
||||
</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter application name to confirm"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-delete-application"
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -20,6 +20,12 @@ interface Props {
|
||||
|
||||
export const CancelQueues = ({ applicationId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.application.cleanQueues.useMutation();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
if (isCloud) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { api } from "@/utils/api";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
import React from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -17,8 +17,15 @@ interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
serverId?: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
||||
export const ShowDeployment = ({
|
||||
logPath,
|
||||
open,
|
||||
onClose,
|
||||
serverId,
|
||||
errorMessage,
|
||||
}: Props) => {
|
||||
const [data, setData] = useState("");
|
||||
const [showExtraLogs, setShowExtraLogs] = useState(false);
|
||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||
@@ -99,6 +106,8 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
||||
}
|
||||
}, [filteredLogs, autoScroll]);
|
||||
|
||||
const optionalErrors = parseLogs(errorMessage || "");
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
@@ -157,9 +166,17 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
||||
<TerminalLine key={index} log={log} noTimestamp />
|
||||
))
|
||||
) : (
|
||||
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
<>
|
||||
{optionalErrors.length > 0 ? (
|
||||
optionalErrors.map((log: LogLine, index: number) => (
|
||||
<TerminalLine key={`extra-${index}`} log={log} noTimestamp />
|
||||
))
|
||||
) : (
|
||||
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { type RouterOutputs, api } from "@/utils/api";
|
||||
import { RocketIcon } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { CancelQueues } from "./cancel-queues";
|
||||
@@ -18,8 +18,11 @@ import { ShowDeployment } from "./show-deployment";
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const ShowDeployments = ({ applicationId }: Props) => {
|
||||
const [activeLog, setActiveLog] = useState<string | null>(null);
|
||||
const [activeLog, setActiveLog] = useState<
|
||||
RouterOutputs["deployment"]["all"][number] | null
|
||||
>(null);
|
||||
const { data } = api.application.one.useQuery({ applicationId });
|
||||
const { data: deployments } = api.deployment.all.useQuery(
|
||||
{ applicationId },
|
||||
@@ -70,15 +73,14 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4">
|
||||
{deployments?.map((deployment) => (
|
||||
{deployments?.map((deployment, index) => (
|
||||
<div
|
||||
key={deployment.deploymentId}
|
||||
className="flex items-center justify-between rounded-lg border p-4 gap-2"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
||||
{deployment.status}
|
||||
|
||||
{index + 1}. {deployment.status}
|
||||
<StatusTooltip
|
||||
status={deployment?.status}
|
||||
className="size-2.5"
|
||||
@@ -100,7 +102,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
setActiveLog(deployment.logPath);
|
||||
setActiveLog(deployment);
|
||||
}}
|
||||
>
|
||||
View
|
||||
@@ -112,9 +114,10 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
||||
)}
|
||||
<ShowDeployment
|
||||
serverId={data?.serverId || ""}
|
||||
open={activeLog !== null}
|
||||
open={Boolean(activeLog && activeLog.logPath !== null)}
|
||||
onClose={() => setActiveLog(null)}
|
||||
logPath={activeLog}
|
||||
logPath={activeLog?.logPath || ""}
|
||||
errorMessage={activeLog?.errorMessage || ""}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -104,9 +104,7 @@ export const AddDomain = ({
|
||||
|
||||
const dictionary = {
|
||||
success: domainId ? "Domain Updated" : "Domain Created",
|
||||
error: domainId
|
||||
? "Error updating the domain"
|
||||
: "Error creating the domain",
|
||||
error: domainId ? "Error updating the domain" : "Error creating the domain",
|
||||
submit: domainId ? "Update" : "Create",
|
||||
dialogDescription: domainId
|
||||
? "In this section you can edit a domain"
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
domainId: string;
|
||||
}
|
||||
export const DeleteDomain = ({ domainId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.domain.delete.useMutation();
|
||||
const utils = api.useUtils();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
domain
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
domainId,
|
||||
})
|
||||
.then((data) => {
|
||||
if (data?.applicationId) {
|
||||
utils.domain.byApplicationId.invalidate({
|
||||
applicationId: data?.applicationId,
|
||||
});
|
||||
utils.application.readTraefikConfig.invalidate({
|
||||
applicationId: data?.applicationId,
|
||||
});
|
||||
} else if (data?.composeId) {
|
||||
utils.domain.byComposeId.invalidate({
|
||||
composeId: data?.composeId,
|
||||
});
|
||||
}
|
||||
|
||||
toast.success("Domain delete successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting the Domain");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -6,19 +7,18 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { ExternalLink, GlobeIcon, PenBoxIcon } from "lucide-react";
|
||||
import { ExternalLink, GlobeIcon, PenBoxIcon, Trash2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
import { AddDomain } from "./add-domain";
|
||||
import { DeleteDomain } from "./delete-domain";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const ShowDomains = ({ applicationId }: Props) => {
|
||||
const { data } = api.domain.byApplicationId.useQuery(
|
||||
const { data, refetch } = api.domain.byApplicationId.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
@@ -26,6 +26,10 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
||||
enabled: !!applicationId,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync: deleteDomain, isLoading: isRemoving } =
|
||||
api.domain.delete.useMutation();
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-5 ">
|
||||
<Card className="bg-background">
|
||||
@@ -69,35 +73,66 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
||||
return (
|
||||
<div
|
||||
key={item.domainId}
|
||||
className="flex w-full items-center gap-4 max-sm:flex-wrap border p-4 rounded-lg"
|
||||
className="flex w-full items-center justify-between gap-4 border p-4 md:px-6 rounded-lg flex-wrap"
|
||||
>
|
||||
<Link
|
||||
className="md:basis-1/2 flex gap-2 items-center hover:underline transition-all w-full"
|
||||
target="_blank"
|
||||
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
|
||||
>
|
||||
<ExternalLink className="size-5" />
|
||||
<span className="truncate max-w-full text-sm">
|
||||
{item.host}
|
||||
</span>
|
||||
<ExternalLink className="size-4 min-w-4" />
|
||||
</Link>
|
||||
|
||||
<Input disabled value={item.host} />
|
||||
<Button variant="outline" disabled>
|
||||
{item.path}
|
||||
</Button>
|
||||
<Button variant="outline" disabled>
|
||||
{item.port}
|
||||
</Button>
|
||||
<Button variant="outline" disabled>
|
||||
{item.https ? "HTTPS" : "HTTP"}
|
||||
</Button>
|
||||
<div className="flex flex-row gap-1">
|
||||
<AddDomain
|
||||
applicationId={applicationId}
|
||||
domainId={item.domainId}
|
||||
>
|
||||
<Button variant="ghost">
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</AddDomain>
|
||||
<DeleteDomain domainId={item.domainId} />
|
||||
<div className="flex gap-8">
|
||||
<div className="flex gap-8 opacity-50 items-center h-10 text-center text-sm font-medium">
|
||||
<span>{item.path}</span>
|
||||
<span>{item.port}</span>
|
||||
<span>{item.https ? "HTTPS" : "HTTP"}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<AddDomain
|
||||
applicationId={applicationId}
|
||||
domainId={item.domainId}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-blue-500/10 "
|
||||
>
|
||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||
</Button>
|
||||
</AddDomain>
|
||||
<DialogAction
|
||||
title="Delete Domain"
|
||||
description="Are you sure you want to delete this domain?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await deleteDomain({
|
||||
domainId: item.domainId,
|
||||
})
|
||||
.then(() => {
|
||||
refetch();
|
||||
toast.success("Domain deleted successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting domain");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10"
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -18,10 +18,11 @@ import { Toggle } from "@/components/ui/toggle";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||
import React, { useEffect, useState } from "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({
|
||||
environment: z.string(),
|
||||
@@ -30,21 +31,39 @@ const addEnvironmentSchema = z.object({
|
||||
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
|
||||
|
||||
interface Props {
|
||||
postgresId: string;
|
||||
id: string;
|
||||
type: Exclude<ServiceType | "compose", "application">;
|
||||
}
|
||||
|
||||
export const ShowPostgresEnvironment = ({ postgresId }: Props) => {
|
||||
export const ShowEnvironment = ({ 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 }),
|
||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||
compose: () =>
|
||||
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
|
||||
};
|
||||
const { data, refetch } = queryMap[type]
|
||||
? queryMap[type]()
|
||||
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||
const { mutateAsync, isLoading } = api.postgres.saveEnvironment.useMutation();
|
||||
|
||||
const { data, refetch } = api.postgres.one.useQuery(
|
||||
{
|
||||
postgresId,
|
||||
},
|
||||
{
|
||||
enabled: !!postgresId,
|
||||
},
|
||||
);
|
||||
const mutationMap = {
|
||||
postgres: () => api.postgres.update.useMutation(),
|
||||
redis: () => api.redis.update.useMutation(),
|
||||
mysql: () => api.mysql.update.useMutation(),
|
||||
mariadb: () => api.mariadb.update.useMutation(),
|
||||
mongo: () => api.mongo.update.useMutation(),
|
||||
compose: () => api.compose.update.useMutation(),
|
||||
};
|
||||
const { mutateAsync, isLoading } = mutationMap[type]
|
||||
? mutationMap[type]()
|
||||
: api.mongo.update.useMutation();
|
||||
|
||||
const form = useForm<EnvironmentSchema>({
|
||||
defaultValues: {
|
||||
environment: "",
|
||||
@@ -62,8 +81,13 @@ export const ShowPostgresEnvironment = ({ postgresId }: Props) => {
|
||||
|
||||
const onSubmit = async (data: EnvironmentSchema) => {
|
||||
mutateAsync({
|
||||
mongoId: id || "",
|
||||
postgresId: id || "",
|
||||
redisId: id || "",
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
composeId: id || "",
|
||||
env: data.environment,
|
||||
postgresId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Environments Added");
|
||||
@@ -111,6 +135,11 @@ export const ShowPostgresEnvironment = ({ postgresId }: Props) => {
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
style={
|
||||
{
|
||||
WebkitTextSecurity: isEnvVisible ? "disc" : null,
|
||||
} as CSSProperties
|
||||
}
|
||||
language="properties"
|
||||
disabled={isEnvVisible}
|
||||
placeholder={`NODE_ENV=production
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { Secrets } from "@/components/ui/secrets";
|
||||
import { api } from "@/utils/api";
|
||||
@@ -56,12 +56,12 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex w-full flex-col gap-5 "
|
||||
>
|
||||
<Card className="bg-background p-6">
|
||||
<Card className="bg-background px-6 pb-6">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex w-full flex-col gap-4"
|
||||
>
|
||||
<Secrets
|
||||
name="env"
|
||||
title="Environment Settings"
|
||||
@@ -89,15 +89,13 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
placeholder="NPM_TOKEN=xyz"
|
||||
/>
|
||||
)}
|
||||
<CardContent>
|
||||
<div className="flex flex-row justify-end">
|
||||
<Button isLoading={isLoading} className="w-fit" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</form>
|
||||
</Form>
|
||||
<div className="flex flex-row justify-end">
|
||||
<Button isLoading={isLoading} className="w-fit" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const DeployApplication = ({ applicationId }: Props) => {
|
||||
const router = useRouter();
|
||||
const { data, refetch } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
|
||||
const { mutateAsync: deploy } = api.application.deploy.useMutation();
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button isLoading={data?.applicationStatus === "running"}>
|
||||
Deploy
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will deploy the application
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await deploy({
|
||||
applicationId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Application deployed successfully");
|
||||
await refetch();
|
||||
router.push(
|
||||
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`,
|
||||
);
|
||||
})
|
||||
|
||||
.catch(() => {
|
||||
toast.error("Error deploying the Application");
|
||||
});
|
||||
|
||||
await refetch();
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -84,7 +84,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
data: repositories,
|
||||
isLoading: isLoadingRepositories,
|
||||
error,
|
||||
isError,
|
||||
} = api.bitbucket.getBitbucketRepositories.useQuery(
|
||||
{
|
||||
bitbucketId,
|
||||
@@ -235,7 +234,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
<CommandGroup>
|
||||
{repositories?.map((repo) => (
|
||||
<CommandItem
|
||||
value={repo.url}
|
||||
value={repo.name}
|
||||
key={repo.url}
|
||||
onSelect={() => {
|
||||
form.setValue("repository", {
|
||||
@@ -245,7 +244,12 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
{repo.name}
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{repo.name}</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{repo.owner.username}
|
||||
</span>
|
||||
</span>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
|
||||
@@ -226,7 +226,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
<CommandGroup>
|
||||
{repositories?.map((repo) => (
|
||||
<CommandItem
|
||||
value={repo.url}
|
||||
value={repo.name}
|
||||
key={repo.url}
|
||||
onSelect={() => {
|
||||
form.setValue("repository", {
|
||||
@@ -236,7 +236,12 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
{repo.name}
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{repo.name}</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{repo.owner.login}
|
||||
</span>
|
||||
</span>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
|
||||
@@ -248,7 +248,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
{repositories?.map((repo) => {
|
||||
return (
|
||||
<CommandItem
|
||||
value={repo.url}
|
||||
value={repo.name}
|
||||
key={repo.url}
|
||||
onSelect={() => {
|
||||
form.setValue("repository", {
|
||||
@@ -260,7 +260,12 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
{repo.name}
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{repo.name}</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{repo.owner.username}
|
||||
</span>
|
||||
</span>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
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, LockIcon, UploadCloud } from "lucide-react";
|
||||
import { GitBranch, UploadCloud } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { SaveBitbucketProvider } from "./save-bitbucket-provider";
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
appName: string;
|
||||
}
|
||||
|
||||
export const ResetApplication = ({ applicationId, appName }: Props) => {
|
||||
const { refetch } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
const { mutateAsync: reload, isLoading } =
|
||||
api.application.reload.useMutation();
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="secondary" isLoading={isLoading}>
|
||||
Reload
|
||||
<RefreshCcw className="size-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will reload the application
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await reload({
|
||||
applicationId,
|
||||
appName,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Service Reloaded");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error reloading the service");
|
||||
});
|
||||
await refetch();
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +1,20 @@
|
||||
import { ShowBuildChooseForm } from "@/components/dashboard/application/build/show";
|
||||
import { ShowProviderForm } from "@/components/dashboard/application/general/generic/show";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { Terminal } from "lucide-react";
|
||||
import React from "react";
|
||||
import { Ban, CheckCircle2, Hammer, RefreshCcw, Terminal } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "sonner";
|
||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||
import { RedbuildApplication } from "../rebuild-application";
|
||||
import { StartApplication } from "../start-application";
|
||||
import { StopApplication } from "../stop-application";
|
||||
import { DeployApplication } from "./deploy-application";
|
||||
import { ResetApplication } from "./reset-application";
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
const router = useRouter();
|
||||
const { data, refetch } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
@@ -25,6 +22,17 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
const { mutateAsync: update } = api.application.update.useMutation();
|
||||
const { mutateAsync: start, isLoading: isStarting } =
|
||||
api.application.start.useMutation();
|
||||
const { mutateAsync: stop, isLoading: isStopping } =
|
||||
api.application.stop.useMutation();
|
||||
|
||||
const { mutateAsync: deploy } = api.application.deploy.useMutation();
|
||||
|
||||
const { mutateAsync: reload, isLoading: isReloading } =
|
||||
api.application.reload.useMutation();
|
||||
|
||||
const { mutateAsync: redeploy } = api.application.redeploy.useMutation();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -33,17 +41,127 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||
<DeployApplication applicationId={applicationId} />
|
||||
<ResetApplication
|
||||
applicationId={applicationId}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
<DialogAction
|
||||
title="Deploy Application"
|
||||
description="Are you sure you want to deploy this application?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await deploy({
|
||||
applicationId: applicationId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Application deployed successfully");
|
||||
refetch();
|
||||
router.push(
|
||||
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`,
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deploying application");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="default"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
>
|
||||
Deploy
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DialogAction
|
||||
title="Reload Application"
|
||||
description="Are you sure you want to reload this application?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await reload({
|
||||
applicationId: applicationId,
|
||||
appName: data?.appName || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Application reloaded successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error reloading application");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button variant="secondary" isLoading={isReloading}>
|
||||
Reload
|
||||
<RefreshCcw className="size-4" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DialogAction
|
||||
title="Rebuild Application"
|
||||
description="Are you sure you want to rebuild this application?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await redeploy({
|
||||
applicationId: applicationId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Application rebuilt successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error rebuilding application");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
>
|
||||
Rebuild
|
||||
<Hammer className="size-4" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
|
||||
<RedbuildApplication applicationId={applicationId} />
|
||||
{data?.applicationStatus === "idle" ? (
|
||||
<StartApplication applicationId={applicationId} />
|
||||
<DialogAction
|
||||
title="Start Application"
|
||||
description="Are you sure you want to start this application?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await start({
|
||||
applicationId: applicationId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Application started successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error starting application");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button variant="secondary" isLoading={isStarting}>
|
||||
Start
|
||||
<CheckCircle2 className="size-4" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
) : (
|
||||
<StopApplication applicationId={applicationId} />
|
||||
<DialogAction
|
||||
title="Stop Application"
|
||||
description="Are you sure you want to stop this application?"
|
||||
onClick={async () => {
|
||||
await stop({
|
||||
applicationId: applicationId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Application stopped successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error stopping application");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button variant="destructive" isLoading={isStopping}>
|
||||
Stop
|
||||
<Ban className="size-4" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
|
||||
@@ -104,9 +104,7 @@ export const AddPreviewDomain = ({
|
||||
|
||||
const dictionary = {
|
||||
success: domainId ? "Domain Updated" : "Domain Created",
|
||||
error: domainId
|
||||
? "Error updating the domain"
|
||||
: "Error creating the domain",
|
||||
error: domainId ? "Error updating the domain" : "Error creating the domain",
|
||||
submit: domainId ? "Update" : "Create",
|
||||
dialogDescription: domainId
|
||||
? "In this section you can edit a domain"
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
@@ -26,7 +25,9 @@ export const ShowPreviewBuilds = ({
|
||||
serverId,
|
||||
trigger,
|
||||
}: Props) => {
|
||||
const [activeLog, setActiveLog] = useState<string | null>(null);
|
||||
const [activeLog, setActiveLog] = useState<
|
||||
RouterOutputs["deployment"]["all"][number] | null
|
||||
>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
@@ -77,7 +78,7 @@ export const ShowPreviewBuilds = ({
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
setActiveLog(deployment.logPath);
|
||||
setActiveLog(deployment);
|
||||
}}
|
||||
>
|
||||
View
|
||||
@@ -89,9 +90,10 @@ export const ShowPreviewBuilds = ({
|
||||
</DialogContent>
|
||||
<ShowDeployment
|
||||
serverId={serverId || ""}
|
||||
open={activeLog !== null}
|
||||
open={Boolean(activeLog && activeLog.logPath !== null)}
|
||||
onClose={() => setActiveLog(null)}
|
||||
logPath={activeLog}
|
||||
logPath={activeLog?.logPath || ""}
|
||||
errorMessage={activeLog?.errorMessage || ""}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
RocketIcon,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import React from "react";
|
||||
import { toast } from "sonner";
|
||||
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
|
||||
import { AddPreviewDomain } from "./add-preview-domain";
|
||||
|
||||
@@ -279,7 +279,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="env"
|
||||
render={({ field }) => (
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Secrets
|
||||
@@ -291,16 +291,6 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
"PORT=3000",
|
||||
].join("\n")}
|
||||
/>
|
||||
{/* <CodeEditor
|
||||
lineWrapping
|
||||
language="properties"
|
||||
wrapperClassName="h-[25rem] font-mono"
|
||||
placeholder={`NODE_ENV=production
|
||||
PORT=3000
|
||||
|
||||
`}
|
||||
{...field}
|
||||
/> */}
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { Hammer } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const RedbuildApplication = ({ applicationId }: Props) => {
|
||||
const { data } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
|
||||
const { mutateAsync } = api.application.redeploy.useMutation();
|
||||
const utils = api.useUtils();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
>
|
||||
Rebuild
|
||||
<Hammer className="size-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Are you sure to rebuild the application?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Is required to deploy at least 1 time in order to reuse the same
|
||||
code
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
toast.success("Redeploying Application....");
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.application.one.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error rebuilding the application");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { CheckCircle2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const StartApplication = ({ applicationId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.application.start.useMutation();
|
||||
const utils = api.useUtils();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="secondary" isLoading={isLoading}>
|
||||
Start
|
||||
<CheckCircle2 className="size-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Are you sure to start the application?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will start the application
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.application.one.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
toast.success("Application started successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error starting the Application");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { Ban } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const StopApplication = ({ applicationId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.application.stop.useMutation();
|
||||
const utils = api.useUtils();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" isLoading={isLoading}>
|
||||
Stop
|
||||
<Ban className="size-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Are you absolutely sure to stop the application?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will stop the application
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.application.one.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
toast.success("Application stopped successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error stopping the Application");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, SquarePen } from "lucide-react";
|
||||
import { PenBoxIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -91,8 +91,12 @@ export const UpdateApplication = ({ applicationId }: Props) => {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<SquarePen className="size-4 text-muted-foreground" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-blue-500/10 "
|
||||
>
|
||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { Package } from "lucide-react";
|
||||
import React from "react";
|
||||
import { AddVolumes } from "../../application/advanced/volumes/add-volumes";
|
||||
import { DeleteVolume } from "../../application/advanced/volumes/delete-volume";
|
||||
import { UpdateVolume } from "../../application/advanced/volumes/update-volume";
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const ShowVolumesCompose = ({ composeId }: Props) => {
|
||||
const { data, refetch } = api.compose.one.useQuery(
|
||||
{
|
||||
composeId,
|
||||
},
|
||||
{ enabled: !!composeId },
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||
<div>
|
||||
<CardTitle className="text-xl">Volumes</CardTitle>
|
||||
<CardDescription>
|
||||
If you want to persist data in this compose use the following config
|
||||
to setup the volumes
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
{data && data?.mounts.length > 0 && (
|
||||
<AddVolumes
|
||||
serviceId={composeId}
|
||||
refetch={refetch}
|
||||
serviceType="compose"
|
||||
>
|
||||
Add Volume
|
||||
</AddVolumes>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{data?.mounts.length === 0 ? (
|
||||
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||
<Package className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
No volumes/mounts configured
|
||||
</span>
|
||||
<AddVolumes
|
||||
serviceId={composeId}
|
||||
refetch={refetch}
|
||||
serviceType="compose"
|
||||
>
|
||||
Add Volume
|
||||
</AddVolumes>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col pt-2 gap-4">
|
||||
<AlertBlock type="info">
|
||||
Please remember to click Redeploy after adding, editing, or
|
||||
deleting a mount to apply the changes.
|
||||
</AlertBlock>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
{data?.mounts.map((mount) => (
|
||||
<div key={mount.mountId}>
|
||||
<div
|
||||
key={mount.mountId}
|
||||
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-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">
|
||||
{mount.type.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
{mount.type === "volume" && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium">Volume Name</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{mount.volumeName}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mount.type === "file" && (
|
||||
<>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium">Content</span>
|
||||
<span className="text-sm text-muted-foreground w-40 truncate">
|
||||
{mount.content}
|
||||
</span>
|
||||
</div>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
{mount.type === "bind" && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium">Host Path</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{mount.hostPath}
|
||||
</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
|
||||
mountId={mount.mountId}
|
||||
type={mount.type}
|
||||
refetch={refetch}
|
||||
serviceType="compose"
|
||||
/>
|
||||
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
@@ -21,9 +20,10 @@ 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 "lucide-react";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
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";
|
||||
@@ -40,16 +40,42 @@ const deleteComposeSchema = z.object({
|
||||
type DeleteCompose = z.infer<typeof deleteComposeSchema>;
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
id: string;
|
||||
type: ServiceType | "application";
|
||||
}
|
||||
|
||||
export const DeleteCompose = ({ composeId }: Props) => {
|
||||
export const DeleteService = ({ id, type }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync, isLoading } = api.compose.delete.useMutation();
|
||||
const { data } = api.compose.one.useQuery(
|
||||
{ composeId },
|
||||
{ enabled: !!composeId },
|
||||
);
|
||||
|
||||
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 }),
|
||||
compose: () =>
|
||||
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
|
||||
};
|
||||
const { data } = queryMap[type]
|
||||
? queryMap[type]()
|
||||
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||
|
||||
const mutationMap = {
|
||||
postgres: () => api.postgres.remove.useMutation(),
|
||||
redis: () => api.redis.remove.useMutation(),
|
||||
mysql: () => api.mysql.remove.useMutation(),
|
||||
mariadb: () => api.mariadb.remove.useMutation(),
|
||||
application: () => api.application.delete.useMutation(),
|
||||
mongo: () => api.mongo.remove.useMutation(),
|
||||
compose: () => api.compose.delete.useMutation(),
|
||||
};
|
||||
const { mutateAsync, isLoading } = mutationMap[type]
|
||||
? mutationMap[type]()
|
||||
: api.mongo.remove.useMutation();
|
||||
const { push } = useRouter();
|
||||
const form = useForm<DeleteCompose>({
|
||||
defaultValues: {
|
||||
@@ -63,14 +89,23 @@ export const DeleteCompose = ({ composeId }: Props) => {
|
||||
const expectedName = `${data?.name}/${data?.appName}`;
|
||||
if (formData.projectName === expectedName) {
|
||||
const { deleteVolumes } = formData;
|
||||
await mutateAsync({ composeId, deleteVolumes })
|
||||
await mutateAsync({
|
||||
mongoId: id || "",
|
||||
postgresId: id || "",
|
||||
redisId: id || "",
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
applicationId: id || "",
|
||||
composeId: id || "",
|
||||
deleteVolumes,
|
||||
})
|
||||
.then((result) => {
|
||||
push(`/dashboard/project/${result?.projectId}`);
|
||||
toast.success("Compose deleted successfully");
|
||||
toast.success("deleted successfully");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting the compose");
|
||||
toast.error("Error deleting the service");
|
||||
});
|
||||
} else {
|
||||
form.setError("projectName", {
|
||||
@@ -82,8 +117,13 @@ export const DeleteCompose = ({ composeId }: Props) => {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10 "
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<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">
|
||||
@@ -91,8 +131,8 @@ export const DeleteCompose = ({ composeId }: Props) => {
|
||||
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||
<DialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
compose. If you are sure please enter the compose name to delete
|
||||
this compose.
|
||||
service. If you are sure please enter the service name to delete
|
||||
this service.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4">
|
||||
@@ -115,9 +155,7 @@ export const DeleteCompose = ({ composeId }: Props) => {
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (data?.name && data?.appName) {
|
||||
navigator.clipboard.writeText(
|
||||
`${data.name}/${data.appName}`,
|
||||
);
|
||||
copy(`${data.name}/${data.appName}`);
|
||||
toast.success("Copied to clipboard. Be careful!");
|
||||
}
|
||||
}}
|
||||
@@ -138,27 +176,29 @@ export const DeleteCompose = ({ composeId }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="deleteVolumes"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
{type === "compose" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="deleteVolumes"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormLabel className="ml-2">
|
||||
Delete volumes associated with this compose
|
||||
</FormLabel>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormLabel className="ml-2">
|
||||
Delete volumes associated with this compose
|
||||
</FormLabel>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
@@ -20,6 +20,11 @@ interface Props {
|
||||
|
||||
export const CancelQueuesCompose = ({ composeId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.compose.cleanQueues.useMutation();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
if (isCloud) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { api } from "@/utils/api";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
import React from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -17,12 +17,14 @@ interface Props {
|
||||
serverId?: string;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
errorMessage?: string;
|
||||
}
|
||||
export const ShowDeploymentCompose = ({
|
||||
logPath,
|
||||
open,
|
||||
onClose,
|
||||
serverId,
|
||||
errorMessage,
|
||||
}: Props) => {
|
||||
const [data, setData] = useState("");
|
||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||
@@ -105,6 +107,8 @@ export const ShowDeploymentCompose = ({
|
||||
}
|
||||
}, [filteredLogs, autoScroll]);
|
||||
|
||||
const optionalErrors = parseLogs(errorMessage || "");
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
@@ -161,9 +165,17 @@ export const ShowDeploymentCompose = ({
|
||||
<TerminalLine key={index} log={log} noTimestamp />
|
||||
))
|
||||
) : (
|
||||
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
<>
|
||||
{optionalErrors.length > 0 ? (
|
||||
optionalErrors.map((log: LogLine, index: number) => (
|
||||
<TerminalLine key={`extra-${index}`} log={log} noTimestamp />
|
||||
))
|
||||
) : (
|
||||
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { type RouterOutputs, api } from "@/utils/api";
|
||||
import { RocketIcon } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { CancelQueuesCompose } from "./cancel-queues-compose";
|
||||
@@ -19,7 +19,9 @@ interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
export const ShowDeploymentsCompose = ({ composeId }: Props) => {
|
||||
const [activeLog, setActiveLog] = useState<string | null>(null);
|
||||
const [activeLog, setActiveLog] = useState<
|
||||
RouterOutputs["deployment"]["all"][number] | null
|
||||
>(null);
|
||||
const { data } = api.compose.one.useQuery({ composeId });
|
||||
const { data: deployments } = api.deployment.allByCompose.useQuery(
|
||||
{ composeId },
|
||||
@@ -100,7 +102,7 @@ export const ShowDeploymentsCompose = ({ composeId }: Props) => {
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
setActiveLog(deployment.logPath);
|
||||
setActiveLog(deployment);
|
||||
}}
|
||||
>
|
||||
View
|
||||
@@ -112,9 +114,10 @@ export const ShowDeploymentsCompose = ({ composeId }: Props) => {
|
||||
)}
|
||||
<ShowDeploymentCompose
|
||||
serverId={data?.serverId || ""}
|
||||
open={activeLog !== null}
|
||||
open={Boolean(activeLog && activeLog.logPath !== null)}
|
||||
onClose={() => setActiveLog(null)}
|
||||
logPath={activeLog}
|
||||
logPath={activeLog?.logPath || ""}
|
||||
errorMessage={activeLog?.errorMessage || ""}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -126,9 +126,7 @@ export const AddDomainCompose = ({
|
||||
|
||||
const dictionary = {
|
||||
success: domainId ? "Domain Updated" : "Domain Created",
|
||||
error: domainId
|
||||
? "Error updating the domain"
|
||||
: "Error creating the domain",
|
||||
error: domainId ? "Error updating the domain" : "Error creating the domain",
|
||||
submit: domainId ? "Update" : "Create",
|
||||
dialogDescription: domainId
|
||||
? "In this section you can edit a domain"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -6,11 +7,10 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { ExternalLink, GlobeIcon, PenBoxIcon } from "lucide-react";
|
||||
import { ExternalLink, GlobeIcon, PenBoxIcon, Trash2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { DeleteDomain } from "../../application/domains/delete-domain";
|
||||
import { toast } from "sonner";
|
||||
import { AddDomainCompose } from "./add-domain";
|
||||
|
||||
interface Props {
|
||||
@@ -18,7 +18,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ShowDomainsCompose = ({ composeId }: Props) => {
|
||||
const { data } = api.domain.byComposeId.useQuery(
|
||||
const { data, refetch } = api.domain.byComposeId.useQuery(
|
||||
{
|
||||
composeId,
|
||||
},
|
||||
@@ -27,6 +27,9 @@ export const ShowDomainsCompose = ({ composeId }: Props) => {
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync: deleteDomain, isLoading: isRemoving } =
|
||||
api.domain.delete.useMutation();
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-5 ">
|
||||
<Card className="bg-background">
|
||||
@@ -70,34 +73,70 @@ export const ShowDomainsCompose = ({ composeId }: Props) => {
|
||||
return (
|
||||
<div
|
||||
key={item.domainId}
|
||||
className="flex w-full items-center gap-4 max-sm:flex-wrap border p-4 rounded-lg"
|
||||
className="flex w-full items-center justify-between gap-4 border p-4 md:px-6 rounded-lg flex-wrap"
|
||||
>
|
||||
<Link target="_blank" href={`http://${item.host}`}>
|
||||
<ExternalLink className="size-5" />
|
||||
</Link>
|
||||
<Button variant="outline" disabled>
|
||||
{item.serviceName}
|
||||
</Button>
|
||||
<Input disabled value={item.host} />
|
||||
<Button variant="outline" disabled>
|
||||
{item.path}
|
||||
</Button>
|
||||
<Button variant="outline" disabled>
|
||||
{item.port}
|
||||
</Button>
|
||||
<Button variant="outline" disabled>
|
||||
{item.https ? "HTTPS" : "HTTP"}
|
||||
</Button>
|
||||
<div className="flex flex-row gap-1">
|
||||
<AddDomainCompose
|
||||
composeId={composeId}
|
||||
domainId={item.domainId}
|
||||
<div className="md:basis-1/2 flex gap-6 w-full items-center">
|
||||
<span className="opacity-50 text-center font-medium text-sm whitespace-nowrap">
|
||||
{item.serviceName}
|
||||
</span>
|
||||
|
||||
<Link
|
||||
className="flex gap-2 items-center hover:underline transition-all w-full max-w-[calc(100%-4rem)]"
|
||||
target="_blank"
|
||||
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
|
||||
>
|
||||
<Button variant="ghost">
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</AddDomainCompose>
|
||||
<DeleteDomain domainId={item.domainId} />
|
||||
<span className="truncate text-sm">{item.host}</span>
|
||||
<ExternalLink className="size-4 min-w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-8">
|
||||
<div className="flex gap-8 opacity-50 items-center h-10 text-center text-sm font-medium">
|
||||
<span>{item.path}</span>
|
||||
<span>{item.port}</span>
|
||||
<span>{item.https ? "HTTPS" : "HTTP"}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<AddDomainCompose
|
||||
composeId={composeId}
|
||||
domainId={item.domainId}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-blue-500/10 "
|
||||
>
|
||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||
</Button>
|
||||
</AddDomainCompose>
|
||||
<DialogAction
|
||||
title="Delete Domain"
|
||||
description="Are you sure you want to delete this domain?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await deleteDomain({
|
||||
domainId: item.domainId,
|
||||
})
|
||||
.then((_data) => {
|
||||
refetch();
|
||||
toast.success("Domain deleted successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting domain");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10"
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} 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 React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const addEnvironmentSchema = z.object({
|
||||
environment: z.string(),
|
||||
});
|
||||
|
||||
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const ShowEnvironmentCompose = ({ composeId }: Props) => {
|
||||
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||
const { mutateAsync, isLoading } = api.compose.update.useMutation();
|
||||
|
||||
const { data, refetch } = api.compose.one.useQuery(
|
||||
{
|
||||
composeId,
|
||||
},
|
||||
{
|
||||
enabled: !!composeId,
|
||||
},
|
||||
);
|
||||
const form = useForm<EnvironmentSchema>({
|
||||
defaultValues: {
|
||||
environment: "",
|
||||
},
|
||||
resolver: zodResolver(addEnvironmentSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
environment: data.env || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
|
||||
const onSubmit = async (data: EnvironmentSchema) => {
|
||||
mutateAsync({
|
||||
env: data.environment,
|
||||
composeId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Environments Added");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error adding environment");
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isEnvVisible) {
|
||||
if (data?.env) {
|
||||
const maskedLines = data.env
|
||||
.split("\n")
|
||||
.map((line) => "*".repeat(line.length))
|
||||
.join("\n");
|
||||
form.reset({
|
||||
environment: maskedLines,
|
||||
});
|
||||
} else {
|
||||
form.reset({
|
||||
environment: "",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
form.reset({
|
||||
environment: data?.env || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form, isEnvVisible]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-5 ">
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row w-full items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||
<CardDescription>
|
||||
You can add environment variables to your resource.
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
<Toggle
|
||||
aria-label="Toggle bold"
|
||||
pressed={isEnvVisible}
|
||||
onPressedChange={setIsEnvVisible}
|
||||
>
|
||||
{isEnvVisible ? (
|
||||
<EyeOffIcon className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<EyeIcon className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</Toggle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="w-full space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="environment"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
language="properties"
|
||||
disabled={isEnvVisible}
|
||||
placeholder={`NODE_ENV=production
|
||||
PORT=3000
|
||||
`}
|
||||
className="h-96 font-mono"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex flex-row justify-end">
|
||||
<Button
|
||||
disabled={isEnvVisible}
|
||||
isLoading={isLoading}
|
||||
className="w-fit"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,28 +1,17 @@
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { CheckCircle2, ExternalLink, Globe, Terminal } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Ban, CheckCircle2, Hammer, Terminal } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "sonner";
|
||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||
import { StartCompose } from "../start-compose";
|
||||
import { DeployCompose } from "./deploy-compose";
|
||||
import { RedbuildCompose } from "./rebuild-compose";
|
||||
import { StopCompose } from "./stop-compose";
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
export const ComposeActions = ({ composeId }: Props) => {
|
||||
const router = useRouter();
|
||||
const { data, refetch } = api.compose.one.useQuery(
|
||||
{
|
||||
composeId,
|
||||
@@ -30,33 +19,109 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
{ enabled: !!composeId },
|
||||
);
|
||||
const { mutateAsync: update } = api.compose.update.useMutation();
|
||||
|
||||
const extractDomains = (env: string) => {
|
||||
const lines = env.split("\n");
|
||||
const hostLines = lines.filter((line) => {
|
||||
const [key, value] = line.split("=");
|
||||
return key?.trim().endsWith("_HOST");
|
||||
});
|
||||
|
||||
const hosts = hostLines.map((line) => {
|
||||
const [key, value] = line.split("=");
|
||||
return value ? value.trim() : "";
|
||||
});
|
||||
|
||||
return hosts;
|
||||
};
|
||||
|
||||
const domains = extractDomains(data?.env || "");
|
||||
|
||||
const { mutateAsync: deploy } = api.compose.deploy.useMutation();
|
||||
const { mutateAsync: redeploy } = api.compose.redeploy.useMutation();
|
||||
const { mutateAsync: start, isLoading: isStarting } =
|
||||
api.compose.start.useMutation();
|
||||
const { mutateAsync: stop, isLoading: isStopping } =
|
||||
api.compose.stop.useMutation();
|
||||
return (
|
||||
<div className="flex flex-row gap-4 w-full flex-wrap ">
|
||||
<DeployCompose composeId={composeId} />
|
||||
<RedbuildCompose composeId={composeId} />
|
||||
<DialogAction
|
||||
title="Deploy Compose"
|
||||
description="Are you sure you want to deploy this compose?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await deploy({
|
||||
composeId: composeId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Compose deployed successfully");
|
||||
refetch();
|
||||
router.push(
|
||||
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`,
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deploying compose");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button variant="default" isLoading={data?.composeStatus === "running"}>
|
||||
Deploy
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DialogAction
|
||||
title="Rebuild Compose"
|
||||
description="Are you sure you want to rebuild this compose?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await redeploy({
|
||||
composeId: composeId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Compose rebuilt successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error rebuilding compose");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={data?.composeStatus === "running"}
|
||||
>
|
||||
Rebuild
|
||||
<Hammer className="size-4" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
{data?.composeType === "docker-compose" &&
|
||||
data?.composeStatus === "idle" ? (
|
||||
<StartCompose composeId={composeId} />
|
||||
<DialogAction
|
||||
title="Start Compose"
|
||||
description="Are you sure you want to start this compose?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await start({
|
||||
composeId: composeId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Compose started successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error starting compose");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button variant="secondary" isLoading={isStarting}>
|
||||
Start
|
||||
<CheckCircle2 className="size-4" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
) : (
|
||||
<StopCompose composeId={composeId} />
|
||||
<DialogAction
|
||||
title="Stop Compose"
|
||||
description="Are you sure you want to stop this compose?"
|
||||
onClick={async () => {
|
||||
await stop({
|
||||
composeId: composeId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Compose stopped successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error stopping compose");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button variant="destructive" isLoading={isStopping}>
|
||||
Stop
|
||||
<Ban className="size-4" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
|
||||
<DockerTerminalModal
|
||||
@@ -89,41 +154,6 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
className="flex flex-row gap-2 items-center"
|
||||
/>
|
||||
</div>
|
||||
{domains.length > 0 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">
|
||||
Domains
|
||||
<Globe className="text-xs size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56">
|
||||
<DropdownMenuLabel>Domains detected</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
{domains.map((host, index) => {
|
||||
const url =
|
||||
host.startsWith("http://") || host.startsWith("https://")
|
||||
? host
|
||||
: `http://${host}`;
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={`domain-${index}`}
|
||||
className="cursor-pointer"
|
||||
asChild
|
||||
>
|
||||
<Link href={url} target="_blank">
|
||||
{host}
|
||||
<ExternalLink className="ml-2 text-xs text-muted-foreground" />
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
||||
import { RandomizeCompose } from "./randomize-compose";
|
||||
import { ShowUtilities } from "./show-utilities";
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
@@ -35,8 +35,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||
{ enabled: !!composeId },
|
||||
);
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.compose.update.useMutation();
|
||||
const { mutateAsync, isLoading } = api.compose.update.useMutation();
|
||||
|
||||
const form = useForm<AddComposeFile>({
|
||||
defaultValues: {
|
||||
@@ -76,7 +75,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||
composeId,
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
.catch((_e) => {
|
||||
toast.error("Error updating the Compose config");
|
||||
});
|
||||
};
|
||||
@@ -125,7 +124,7 @@ services:
|
||||
</Form>
|
||||
<div className="flex justify-between flex-col lg:flex-row gap-2">
|
||||
<div className="w-full flex flex-col lg:flex-row gap-4 items-end">
|
||||
<RandomizeCompose composeId={composeId} />
|
||||
<ShowUtilities composeId={composeId} />
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const DeployCompose = ({ composeId }: Props) => {
|
||||
const router = useRouter();
|
||||
const { data, refetch } = api.compose.one.useQuery(
|
||||
{
|
||||
composeId,
|
||||
},
|
||||
{ enabled: !!composeId },
|
||||
);
|
||||
|
||||
const { mutateAsync: deploy } = api.compose.deploy.useMutation();
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button isLoading={data?.composeStatus === "running"}>Deploy</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will deploy the compose
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
toast.success("Deploying Compose....");
|
||||
|
||||
await refetch();
|
||||
await deploy({
|
||||
composeId,
|
||||
})
|
||||
.then(async () => {
|
||||
router.push(
|
||||
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`,
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deploying Compose");
|
||||
});
|
||||
|
||||
await refetch();
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -84,7 +84,6 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
data: repositories,
|
||||
isLoading: isLoadingRepositories,
|
||||
error,
|
||||
isError,
|
||||
} = api.bitbucket.getBitbucketRepositories.useQuery(
|
||||
{
|
||||
bitbucketId,
|
||||
@@ -237,7 +236,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
<CommandGroup>
|
||||
{repositories?.map((repo) => (
|
||||
<CommandItem
|
||||
value={repo.url}
|
||||
value={repo.name}
|
||||
key={repo.url}
|
||||
onSelect={() => {
|
||||
form.setValue("repository", {
|
||||
@@ -247,7 +246,12 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
{repo.name}
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{repo.name}</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{repo.owner.username}
|
||||
</span>
|
||||
</span>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
|
||||
@@ -228,7 +228,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
<CommandGroup>
|
||||
{repositories?.map((repo) => (
|
||||
<CommandItem
|
||||
value={repo.url}
|
||||
value={repo.name}
|
||||
key={repo.url}
|
||||
onSelect={() => {
|
||||
form.setValue("repository", {
|
||||
@@ -238,7 +238,12 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
{repo.name}
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{repo.name}</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{repo.owner.login}
|
||||
</span>
|
||||
</span>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
|
||||
@@ -250,7 +250,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
{repositories?.map((repo) => {
|
||||
return (
|
||||
<CommandItem
|
||||
value={repo.url}
|
||||
value={repo.name}
|
||||
key={repo.url}
|
||||
onSelect={() => {
|
||||
form.setValue("repository", {
|
||||
@@ -262,7 +262,12 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
{repo.name}
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{repo.name}</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{repo.owner.username}
|
||||
</span>
|
||||
</span>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { api } from "@/utils/api";
|
||||
import { CodeIcon, GitBranch, LockIcon } from "lucide-react";
|
||||
import { CodeIcon, GitBranch } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { ComposeFileEditor } from "../compose-file-editor";
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
} from "@/components/ui/form";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
const schema = z.object({
|
||||
isolatedDeployment: z.boolean().optional(),
|
||||
});
|
||||
|
||||
type Schema = z.infer<typeof schema>;
|
||||
|
||||
export const IsolatedDeployment = ({ composeId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [compose, setCompose] = useState<string>("");
|
||||
const { mutateAsync, error, isError } =
|
||||
api.compose.isolatedDeployment.useMutation();
|
||||
|
||||
const { mutateAsync: updateCompose } = api.compose.update.useMutation();
|
||||
|
||||
const { data, refetch } = api.compose.one.useQuery(
|
||||
{ composeId },
|
||||
{ enabled: !!composeId },
|
||||
);
|
||||
|
||||
console.log(data);
|
||||
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
isolatedDeployment: false,
|
||||
},
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
randomizeCompose();
|
||||
if (data) {
|
||||
form.reset({
|
||||
isolatedDeployment: data?.isolatedDeployment || false,
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||
|
||||
const onSubmit = async (formData: Schema) => {
|
||||
await updateCompose({
|
||||
composeId,
|
||||
isolatedDeployment: formData?.isolatedDeployment || false,
|
||||
})
|
||||
.then(async (_data) => {
|
||||
randomizeCompose();
|
||||
refetch();
|
||||
toast.success("Compose updated");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating the compose");
|
||||
});
|
||||
};
|
||||
|
||||
const randomizeCompose = async () => {
|
||||
await mutateAsync({
|
||||
composeId,
|
||||
suffix: data?.appName || "",
|
||||
})
|
||||
.then(async (data) => {
|
||||
await utils.project.all.invalidate();
|
||||
setCompose(data);
|
||||
toast.success("Compose Isolated");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error isolating the compose");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Isolate Deployment</DialogTitle>
|
||||
<DialogDescription>
|
||||
Use this option to isolate the deployment of this compose file.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<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 volumes</li>
|
||||
<li>Docker networks</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
id="hook-form-add-project"
|
||||
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 Randomize ({data?.appName})</FormLabel>
|
||||
<FormDescription>
|
||||
Enable randomize 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="hook-form-add-project"
|
||||
type="submit"
|
||||
className="lg:w-fit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Label>Preview</Label>
|
||||
<pre>
|
||||
<CodeEditor
|
||||
value={compose || ""}
|
||||
language="yaml"
|
||||
readOnly
|
||||
height="50rem"
|
||||
/>
|
||||
</pre>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,14 +1,10 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
@@ -20,15 +16,10 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
} from "@/components/ui/input-otp";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, Dices } from "lucide-react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -48,7 +39,7 @@ type Schema = z.infer<typeof schema>;
|
||||
export const RandomizeCompose = ({ composeId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [compose, setCompose] = useState<string>("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [_isOpen, _setIsOpen] = useState(false);
|
||||
const { mutateAsync, error, isError } =
|
||||
api.compose.randomizeCompose.useMutation();
|
||||
|
||||
@@ -70,6 +61,7 @@ export const RandomizeCompose = ({ composeId }: Props) => {
|
||||
const suffix = form.watch("suffix");
|
||||
|
||||
useEffect(() => {
|
||||
randomizeCompose();
|
||||
if (data) {
|
||||
form.reset({
|
||||
suffix: data?.suffix || "",
|
||||
@@ -84,7 +76,7 @@ export const RandomizeCompose = ({ composeId }: Props) => {
|
||||
suffix: formData?.suffix || "",
|
||||
randomize: formData?.randomize || false,
|
||||
})
|
||||
.then(async (data) => {
|
||||
.then(async (_data) => {
|
||||
randomizeCompose();
|
||||
refetch();
|
||||
toast.success("Compose updated");
|
||||
@@ -110,126 +102,117 @@ export const RandomizeCompose = ({ composeId }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild onClick={() => randomizeCompose()}>
|
||||
<Button className="max-lg:w-full" variant="outline">
|
||||
<Dices className="h-4 w-4" />
|
||||
Randomize Compose
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-6xl max-h-[50rem] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Randomize Compose (Experimental)</DialogTitle>
|
||||
<DialogDescription>
|
||||
Use this in case you want to deploy the same compose file and you
|
||||
have conflicts with some property like volumes, networks, etc.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="text-sm text-muted-foreground flex flex-col gap-2">
|
||||
<span>
|
||||
This will randomize the compose file and will add a suffix to the
|
||||
property to avoid conflicts
|
||||
</span>
|
||||
<ul className="list-disc list-inside">
|
||||
<li>volumes</li>
|
||||
<li>networks</li>
|
||||
<li>services</li>
|
||||
<li>configs</li>
|
||||
<li>secrets</li>
|
||||
</ul>
|
||||
<AlertBlock type="info">
|
||||
When you activate this option, we will include a env
|
||||
`COMPOSE_PREFIX` variable to the compose file so you can use it in
|
||||
your compose file.
|
||||
</AlertBlock>
|
||||
</div>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
id="hook-form-add-project"
|
||||
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="suffix"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col justify-center max-sm:items-center w-full">
|
||||
<FormLabel>Suffix</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter a suffix (Optional, example: prod)"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="randomize"
|
||||
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>Apply Randomize</FormLabel>
|
||||
<FormDescription>
|
||||
Apply randomize 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="hook-form-add-project"
|
||||
type="submit"
|
||||
className="lg:w-fit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
await randomizeCompose();
|
||||
}}
|
||||
className="lg:w-fit"
|
||||
>
|
||||
Random
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Randomize Compose (Experimental)</DialogTitle>
|
||||
<DialogDescription>
|
||||
Use this in case you want to deploy the same compose file and you have
|
||||
conflicts with some property like volumes, networks, etc.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="text-sm text-muted-foreground flex flex-col gap-2">
|
||||
<span>
|
||||
This will randomize the compose file and will add a suffix to the
|
||||
property to avoid conflicts
|
||||
</span>
|
||||
<ul className="list-disc list-inside">
|
||||
<li>volumes</li>
|
||||
<li>networks</li>
|
||||
<li>services</li>
|
||||
<li>configs</li>
|
||||
<li>secrets</li>
|
||||
</ul>
|
||||
<AlertBlock type="info">
|
||||
When you activate this option, we will include a env `COMPOSE_PREFIX`
|
||||
variable to the compose file so you can use it in your compose file.
|
||||
</AlertBlock>
|
||||
</div>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
id="hook-form-add-project"
|
||||
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>
|
||||
<pre>
|
||||
<CodeEditor
|
||||
value={compose || ""}
|
||||
language="yaml"
|
||||
readOnly
|
||||
height="50rem"
|
||||
)}
|
||||
|
||||
<div className="flex flex-col lg:flex-col gap-4 w-full ">
|
||||
<div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="suffix"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col justify-center max-sm:items-center w-full mt-4">
|
||||
<FormLabel>Suffix</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter a suffix (Optional, example: prod)"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</pre>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="randomize"
|
||||
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>Apply Randomize</FormLabel>
|
||||
<FormDescription>
|
||||
Apply randomize 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="hook-form-add-project"
|
||||
type="submit"
|
||||
className="lg:w-fit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
await randomizeCompose();
|
||||
}}
|
||||
className="lg:w-fit"
|
||||
>
|
||||
Random
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<pre>
|
||||
<CodeEditor
|
||||
value={compose || ""}
|
||||
language="yaml"
|
||||
readOnly
|
||||
height="50rem"
|
||||
/>
|
||||
</pre>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user