mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-19 07:01:35 +00:00
Compare commits
1429 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7f44f65bc | |||
| 62bd8e3c95 | |||
| 85734c0a24 | |||
| 8d18aeda45 | |||
| 45923d3a1f | |||
| 043843f714 | |||
| 7dda252b7c | |||
| bf0668c319 | |||
| fc1dbcf51a | |||
| b34987530e | |||
| ff8d922f2b | |||
| 01c33ad98b | |||
| 9816ecaea1 | |||
| 832fa526dd | |||
| 2a5eceb555 | |||
| 08d7c4e1c3 | |||
| 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 | |||
| 973b54408e | |||
| 638fbe17a6 | |||
| 75e0d0ba78 | |||
| 0f1b911236 | |||
| da148a6c50 | |||
| c168648cce | |||
| efd3ad4102 | |||
| a9577395eb | |||
| 1d02d4308f | |||
| e93e15a9c8 | |||
| 511a9beaa5 | |||
| ac8c180ba6 | |||
| 9a2898ba4f | |||
| 2c258c84eb | |||
| 36511f34f8 | |||
| fee5bbc535 | |||
| 84ad9a5bb3 | |||
| 69576988eb | |||
| d65d050494 | |||
| 46a5adf793 | |||
| 5558ee3248 | |||
| 5d8ebd027e | |||
| 9aa49ce9be | |||
| 7bcfc17fe9 | |||
| 5d2d4104f0 | |||
| 2af8d6f565 | |||
| ba45b27608 | |||
| 5aa1c6efb7 | |||
| e9dad8f9f8 | |||
| 0c2a382541 | |||
| 81408985d4 | |||
| d45b558251 | |||
| a30b1a0cb8 | |||
| c946e3e01b | |||
| eb7bae2ef5 | |||
| 4533b193a4 | |||
| c22f744e6c | |||
| d3663eba6b | |||
| f9e4a71144 | |||
| 985b8bc2e0 | |||
| 87ef889114 | |||
| f3494922be | |||
| 27252cf58d | |||
| f69fb7684b | |||
| 20a7995d73 | |||
| 6df66c3871 | |||
| cbfdda1928 | |||
| 766279f265 | |||
| f6e4ae700a | |||
| 8c8ffe04a7 | |||
| f242f5c65e | |||
| 46348f43f6 | |||
| c3476a1fdf | |||
| 379ea2ac65 | |||
| a172abaee4 | |||
| a325b293b6 | |||
| 364c2e192e | |||
| d4a3c5cff9 | |||
| 229a9a3a5e | |||
| 5c1993a647 | |||
| 64a449a09d | |||
| e65e6d225f | |||
| b4fcdc433e | |||
| 5ce6172187 | |||
| 0affeea5dd | |||
| 8f76d520c8 | |||
| 566d9e0bee | |||
| da858e215d | |||
| f672c429c4 | |||
| ce34fe3cd8 | |||
| d8dbdb2b9e | |||
| 4065ad4428 | |||
| e035062a10 | |||
| 36a1daae4b | |||
| 830a254837 | |||
| b8580d69d6 | |||
| e3f1518b0d | |||
| ce19a42aee | |||
| 28a2ab9aa5 | |||
| 0535d780b1 | |||
| e1dd666e24 | |||
| ce71fa4f4d | |||
| 364d04f238 | |||
| 0db98c0b92 | |||
| 8410d94283 | |||
| 56cfd35e7d | |||
| e7beb5c75b | |||
| 9c5a61e42f | |||
| 0ee5a6f13e | |||
| 1d35d218ca | |||
| 96cdffb5b9 | |||
| 353effd720 | |||
| 8bfe1632fa | |||
| ed543e5397 | |||
| c6892ba188 | |||
| fa710d4855 | |||
| 375decebb2 | |||
| be2e70a17e | |||
| 3b4214e040 | |||
| 43a493bb5a | |||
| 455cae6b8c | |||
| 869843d9ac | |||
| d2b662f547 | |||
| 31336152ce | |||
| 6afd443257 | |||
| 1d023ac9f3 | |||
| 49616e53ea | |||
| a32e934969 | |||
| eb495b7b99 | |||
| 65ddc22010 | |||
| 7a5b9e3b76 | |||
| 5a302d3c47 | |||
| 5c5066bc72 | |||
| 228d12a61c | |||
| fd0a9b8b58 | |||
| 1ea7d2e1bf | |||
| 7e08c8881e | |||
| e68d867d31 | |||
| a53929a787 | |||
| ec8eaf6249 | |||
| 0d58935ad1 | |||
| f7c8324c4b | |||
| c0e9670daf | |||
| a710728e77 | |||
| 80bd80b786 | |||
| 6e262cde0d | |||
| 21b2cce7a0 | |||
| 870e074825 | |||
| 055b59e6fa | |||
| 8c06296503 | |||
| 29ce8908ee | |||
| 839e1c0f9f | |||
| 54dd531a26 | |||
| 7ebf5ad0f9 | |||
| b85163d935 | |||
| a953e59327 | |||
| b2661e4533 | |||
| 883459624e | |||
| 6e2b2d564b | |||
| 065963857c | |||
| a0c9df4bd4 | |||
| 68c8c70260 | |||
| a926f28d30 | |||
| 59c0636fb0 | |||
| ae159c5678 | |||
| 0abf62dd52 | |||
| e2b155280a | |||
| e42e9bec17 | |||
| 978324e2bf | |||
| 8f05f06259 | |||
| 392be2cfa2 | |||
| 18e89df9a5 | |||
| 4d2a9f8aa7 | |||
| d08530d451 | |||
| 6c9b12cee9 | |||
| a8ff6c7b3f | |||
| 8699e024ee | |||
| 73782ffd26 | |||
| 7a8bb8f71d | |||
| 18eae9f7d7 | |||
| 1aae523a0b | |||
| f40e802331 | |||
| d979aa17c2 | |||
| e2d20fb0e3 | |||
| 62f59c1f9a | |||
| 93e1071057 | |||
| 788771c5eb | |||
| ab9aa56c48 | |||
| 4565b3d7a2 | |||
| c8514e3a1b | |||
| a06dd17aa1 | |||
| 256534570b | |||
| 2804748118 | |||
| e6bc40e7fe | |||
| 196603126b | |||
| a5cd8f18cd | |||
| b842887bc3 | |||
| dd64b06340 | |||
| d9a1976cc0 | |||
| fdfa927532 | |||
| bf2551b0f6 | |||
| ed8be62ff3 | |||
| 77336a21f9 | |||
| e05d01788f | |||
| 651e81ce6d | |||
| fac29b70a5 | |||
| 95eaab43df | |||
| abdef13b93 | |||
| 65f397e1b1 | |||
| 1ae96297e8 | |||
| c51b502116 | |||
| 5a42b78098 | |||
| b39c0ef915 | |||
| 844d582147 | |||
| 0b51088489 | |||
| 9d1cf3736b | |||
| 3a95474662 | |||
| 3858205e52 | |||
| 1dece58cff | |||
| 06b8c82484 | |||
| 8ea453f444 | |||
| 63c0912849 | |||
| 6211a19805 | |||
| d22330f983 | |||
| 8642d8235e | |||
| b52f57cb0d | |||
| d4d74d3831 | |||
| 9d497142db | |||
| 852895c382 | |||
| 20d5913820 | |||
| f1b4a73158 | |||
| 3830f6c4ee | |||
| 5c8eda2405 | |||
| 6bf85bcfa3 | |||
| bc03e718bf | |||
| a941efb1ff | |||
| fe2de6b899 | |||
| 752c9f2818 | |||
| 577b126e66 | |||
| be237ae4cf | |||
| 3080926a50 | |||
| e3ee89104b | |||
| f98f18b331 | |||
| 8505236263 | |||
| b3313cf975 | |||
| 4e31d8ac02 | |||
| 536507377d | |||
| 763219e859 | |||
| 3fc5bfc5c5 | |||
| 813da8f811 | |||
| 5716954665 | |||
| 04d3eb9ec0 | |||
| b592a025e4 | |||
| 6db9c99080 | |||
| 7e8953ff44 | |||
| 81c85ce155 | |||
| bd16e03602 | |||
| 87a5ce2053 | |||
| ca4820940e | |||
| 71fe6de9cb | |||
| 9ff4968e61 | |||
| 2312ae1c12 | |||
| b03011a94f | |||
| 7577e40b25 | |||
| 75e34285ef | |||
| 8e5b0988cf | |||
| 038df9c8a7 | |||
| 829aa2a63c | |||
| 91e90fc379 | |||
| a1e13ee964 | |||
| 341af1bd07 | |||
| 8a274d10eb | |||
| 6c586f9606 | |||
| dcb1ea37c3 | |||
| 58c2ceb355 | |||
| beae03b53d | |||
| 55ec25f5e8 | |||
| 9382acb40c | |||
| c0acdc5df1 | |||
| 413536a336 | |||
| 190f45b3a8 | |||
| e6c242a064 | |||
| c2fe1eed01 | |||
| 676082fc5b | |||
| b676b1a2de | |||
| 5885712c6a | |||
| afedeede16 | |||
| 5f09018199 | |||
| 9d37876bc4 | |||
| 775107ec24 | |||
| 7725b3ca36 | |||
| 5f297fd984 | |||
| 86aba9ce3e | |||
| c6e512bec1 | |||
| fc2b0abdb1 | |||
| d20f86ffe1 | |||
| 1157e08aa1 | |||
| e643255a67 | |||
| 7521bc8297 | |||
| a63981fa15 | |||
| ea0f797d0f | |||
| 181a2ca3c9 | |||
| 3fe057c7f8 | |||
| 1e834ed1d9 | |||
| 9f84545fc7 | |||
| 690a2e7467 | |||
| 995d9004f3 | |||
| ef89d05077 | |||
| 0a3ab7ceac | |||
| 2fd4d580d5 | |||
| 33e2fa3ce3 | |||
| d320847da4 | |||
| 9e84bf324e | |||
| db469e60ad | |||
| 0f949b3273 | |||
| 166b65c50e | |||
| 274c65cbcd | |||
| b538a632d9 | |||
| 765c6442cb | |||
| 115ed7e7bf | |||
| 0644842305 | |||
| a9c62b47ef | |||
| 138650d561 | |||
| 280be5c9df | |||
| 7726fa6112 | |||
| c71d12fd06 | |||
| 3df3d187e4 | |||
| 8ce9db8dd6 | |||
| 6773458da3 | |||
| 92c2a83d92 | |||
| 3decbd5207 | |||
| 8779c67b71 | |||
| 4dc7d9e3c8 | |||
| a439286e5f | |||
| e5d5a98bab | |||
| 4311ba93f3 | |||
| e0b596ec76 | |||
| 379ba20930 | |||
| 236e511adc | |||
| 0b37e171c5 | |||
| 1df1e7b50b | |||
| f15a5bc22d | |||
| 469871d383 | |||
| e22b6ab9be | |||
| b01b05077d | |||
| 22122361ba | |||
| 87c1ce68b9 | |||
| 4c8619677b | |||
| 7f705e31d3 | |||
| 20432ebc3f | |||
| a51a7a82d2 | |||
| 5ba19686c8 | |||
| 22a2e64563 | |||
| 37ee89e6ab | |||
| cb487b8be0 | |||
| 26f8719e5f | |||
| 3bc1bd5b15 | |||
| ee622b1ba0 | |||
| fe088bad3b | |||
| d374f5eedf | |||
| 1c498ee2d2 | |||
| d7e5eb6dfd | |||
| f71e04eaaa | |||
| fc9808e295 | |||
| bc2a286e1d | |||
| 6c582eb91d | |||
| e3b2a401a7 | |||
| 6c55143e96 | |||
| 19a0550b32 | |||
| bb31bef8bc | |||
| abc606d8d9 | |||
| 749dd03fe6 | |||
| 858d7e5c11 | |||
| 079b7b8e72 | |||
| 179f3818f0 | |||
| 8546031df0 | |||
| 16ca198eb4 | |||
| 9b5b452d90 | |||
| 2fa6f3bfa6 | |||
| 42f3105f69 | |||
| a08ba7e8b5 | |||
| a51ada4a1e | |||
| 50b1de9594 | |||
| cb90281583 | |||
| 20b253e708 | |||
| 9a51e0a00d | |||
| 49b812e462 | |||
| 7233667d49 | |||
| 95cd410825 | |||
| 5b8ebdaaa4 | |||
| 343d5ae6a2 | |||
| e16ce0c817 | |||
| 3b2440b1db | |||
| 2f72ccbea7 | |||
| be47f6d09a | |||
| 9a65bf8e21 | |||
| 15959fa91f | |||
| f0c14d144c | |||
| 725b763aa8 | |||
| c0bfd7dde7 | |||
| 31ba5a784d | |||
| 00f9e262a9 | |||
| 54b6a850b7 | |||
| 029cbf4498 | |||
| 8a1cba470c | |||
| 84bb98c7e6 | |||
| cbf0f37a49 | |||
| 2c22aa3689 | |||
| 46289305e8 | |||
| 0b3e15aabc | |||
| 69a3583717 | |||
| bc55fde6d6 | |||
| 1cb1da8097 | |||
| 0698ac8318 | |||
| 7ced6840fa | |||
| 22e6d07f60 | |||
| 6a9690fe3c | |||
| 1c02478688 | |||
| bbe72ad584 | |||
| 83b3176f6f | |||
| 7ecd1627c8 | |||
| 96fbfa7ef5 | |||
| 6874ede933 | |||
| 012f8ff2f5 | |||
| 9a7ed91a55 | |||
| 79b733536f | |||
| 13e9a50959 | |||
| d424ed23f5 | |||
| bf9abbc37c | |||
| b66e8c8855 | |||
| ce0e9ccddc | |||
| e03aef8e37 | |||
| d0be2a2090 | |||
| 30eb1719b0 | |||
| 9e7299517f | |||
| cd061916df | |||
| 6f818a833c | |||
| 00764ffd43 | |||
| e2a16a5723 | |||
| 73b622eb2f | |||
| cf3cea6146 | |||
| b8e41e970d | |||
| 60b0ae5053 | |||
| ff8263d8f6 | |||
| f65822ca7e | |||
| 3feead31d9 | |||
| b1fd1fb306 | |||
| 46cd22038b | |||
| 5058d9b47d | |||
| ddf95d87bd | |||
| 28a5be984d | |||
| 1650f5ca79 | |||
| e023cad72d | |||
| 49559ebee6 | |||
| 6cf0ecf016 | |||
| 1562396339 | |||
| f94ee8c299 | |||
| f47335efe5 | |||
| c8b5889414 | |||
| 124d81bb1c | |||
| 5f987d28c7 | |||
| 32b19a0fb6 | |||
| 5f71a393be | |||
| f4bd729f65 | |||
| 3320e21958 | |||
| 2960d81829 | |||
| 1c1e52f777 | |||
| 64e6919211 | |||
| a53daed434 | |||
| 791c8afab7 | |||
| 4c45be1447 | |||
| 1056810170 | |||
| b3ca81d2e8 | |||
| 9d170e5f46 | |||
| 3d0b6eb368 | |||
| 5db407b674 | |||
| 556a847054 | |||
| 4c34643287 | |||
| 5e590c1ce8 | |||
| 2f15f34a19 | |||
| 7f53e9cf07 | |||
| 48e3d48ab4 | |||
| b9faf4bd1a | |||
| 27f43e774a | |||
| c9d3616088 | |||
| 7fe8cd03bf | |||
| 38e5d244fe | |||
| 14573f90f7 | |||
| cbbbe44802 | |||
| 00c7ae3f40 | |||
| f10eae40c7 | |||
| df63182f39 | |||
| 626bf7c41b | |||
| 75abc4758a | |||
| c4c4b459cc | |||
| d8787ec11d | |||
| 40c97b8e9c | |||
| fd0a472468 | |||
| db27ec0372 | |||
| 841b264257 | |||
| 5f6516ab7d | |||
| 2daa159e29 | |||
| 262a2394a9 | |||
| e7383e1323 | |||
| 9a8a40b0f8 | |||
| bcf1ba242e | |||
| a235815a13 | |||
| 57594ecb0c | |||
| 572579af91 | |||
| 63998f71ec | |||
| 45fd2d149c | |||
| 9a35c85277 | |||
| 0cf21cf3f7 | |||
| 7400913646 | |||
| e78d354d0d | |||
| bec3ad6bb5 | |||
| 5846e429e5 | |||
| 0f7652d02c | |||
| fef19056fa | |||
| b07d9939a6 | |||
| 301e3480e4 | |||
| 976ac053f7 | |||
| f102bae5d5 | |||
| 00883dde11 | |||
| e194f3c454 | |||
| cdd39670f5 | |||
| 88f7cf2546 | |||
| 34ea7ad8c9 | |||
| 081a2d8f69 | |||
| a6368ee0b8 | |||
| 4132f714ae | |||
| 333776a5a1 | |||
| 5853117e5f | |||
| 9e0e3540f5 | |||
| 7bd6e7fd9a | |||
| 95ab6af3ac | |||
| 69876029b1 | |||
| d4fdf881cd | |||
| 3b14ebcaa4 | |||
| 22b8fa2c00 | |||
| 714865730f | |||
| 7469c30992 | |||
| b296b6bbf0 | |||
| 37fa139a65 | |||
| a1cf597c2b | |||
| c8e9d9d169 | |||
| d01928a878 | |||
| 30c19c5698 | |||
| 01dfa7feaf | |||
| 58e6462ff1 | |||
| d18876d4fb | |||
| 492c912c61 | |||
| 6a283c8ee2 | |||
| 59dfdd6192 | |||
| 3c072d7aa8 | |||
| 19d897f3ad | |||
| 0477329db7 | |||
| fabe946526 | |||
| daa0c9d5d4 | |||
| afe9b3c113 | |||
| cbfd09786a | |||
| 54eb5544ac | |||
| ac33b6b6a1 | |||
| 653b1972ca | |||
| 7d7eb6a7a2 | |||
| fab7e138b7 | |||
| 62b635b2f0 | |||
| 2dd352ee76 | |||
| 422b6eea82 | |||
| 4850305fb6 | |||
| 97779f5686 | |||
| d4b8985d71 | |||
| d5686063e0 | |||
| 62ca8eec53 | |||
| 204143648d | |||
| be8bd78bcc | |||
| 9003e43702 | |||
| 55ac24ee8e | |||
| f3be56234b | |||
| fd59beaff1 | |||
| 4a70d60aed | |||
| f7533c88f6 | |||
| ea8cae7815 | |||
| e9956a66da | |||
| 2eeb4017ac | |||
| 28f0c9f162 | |||
| 4967d3bb31 | |||
| 238fa5d02d | |||
| d1436c992e | |||
| 0654804821 | |||
| c0876044b0 | |||
| 6dff11af22 | |||
| 6d674a4c6b | |||
| 96b2579d69 | |||
| 0708fa05b6 | |||
| 597842a99f | |||
| 105cf1014f | |||
| b93f36ae77 | |||
| bebb4b973c | |||
| f790530d4d | |||
| b7374549b8 | |||
| 366e881d72 | |||
| c2125d82b1 | |||
| 32b3a76457 | |||
| 5cbdc8fad9 | |||
| 3698e8a827 | |||
| a83b62f62b | |||
| ac033cea22 | |||
| 58814239d9 | |||
| 6fc1ce2fbc | |||
| adde8126ab | |||
| cda66606ec | |||
| 28f2c1a3c0 | |||
| 1c5fe8a283 | |||
| da005bc511 | |||
| 5c8721406a | |||
| 5db5336ec8 | |||
| a6e7edd4d9 | |||
| ddbb414225 | |||
| b73889dd4f | |||
| ce2dce3401 | |||
| 173110a415 | |||
| 2307346ae3 | |||
| 2f175f0e44 | |||
| 7003fe77c9 | |||
| 04235fb6c9 | |||
| f138b0917f | |||
| 82367213ea | |||
| 3c490ba2d0 | |||
| 4bf5e5ca06 | |||
| 6af5742702 | |||
| 3015d69adc | |||
| f7fa8e74af | |||
| 2835c997e9 | |||
| bd55e3751f | |||
| 74374bd643 | |||
| 4e929c12f2 | |||
| 56ea356723 | |||
| b1f7d05743 | |||
| 58338380ab | |||
| 2487e3e062 | |||
| 01a882497f | |||
| 036313e3c3 | |||
| d3304052b0 | |||
| 9efd2e3d5c | |||
| 1e7d9fc3aa | |||
| 70ca219c0a | |||
| 3a07d8de2e | |||
| f4f1fc28a0 | |||
| 4c71d3a95f | |||
| af84942d22 | |||
| 1aaff0594d | |||
| 69a4a87079 | |||
| 3eef4aa016 | |||
| 2492581bde | |||
| be934065d9 | |||
| 82fc9897d2 | |||
| 8162dcfb71 | |||
| b6ab653ef3 | |||
| 17e9a1a497 | |||
| 46f7d43595 | |||
| 6961ee1fc0 | |||
| 8e532d5a60 | |||
| fa791706a0 | |||
| a5c1f8ef49 | |||
| 6866c3b116 | |||
| 444302e7b9 | |||
| 9b77573269 | |||
| 8157dd9eaa | |||
| f1fc3f161a | |||
| 06af2042ee | |||
| 8900e30ae7 | |||
| b5fa411093 | |||
| cab9443d25 | |||
| de315124c3 | |||
| 7bef3a0c29 | |||
| 82afd486da | |||
| 69c9e86a13 | |||
| 3d59e289be | |||
| 59bb59ee24 | |||
| 8d33ff5fb5 | |||
| 46219e1b3d | |||
| 3457de4f36 | |||
| e57efa2e31 | |||
| fb0308fd60 | |||
| b376ead7b5 | |||
| d081d477ef | |||
| cf73f1f764 | |||
| deeea11428 | |||
| 5c17797749 | |||
| 7b06fd47b8 | |||
| faceed12b0 | |||
| 814580ff2c | |||
| 4f092b2fb3 | |||
| 0799f8e04c | |||
| 59c050b519 | |||
| 88f969917f | |||
| ed470ee827 | |||
| 96584e5b32 | |||
| a7165bef20 | |||
| b0f5e7dad3 | |||
| fa083257f1 | |||
| 5cf12e51d1 | |||
| b6fd410af2 | |||
| c5c3ca39cd | |||
| 0e433a3d36 | |||
| b08a2f54f0 | |||
| 29ffdf2c71 | |||
| 58b185f6dd | |||
| 5f6d041248 | |||
| b817b4b6ee | |||
| 819de5a32e | |||
| 65ee0a3e22 | |||
| e0fe4e4995 | |||
| 461d30fc26 | |||
| 3fee18d06e | |||
| 046923aeb3 | |||
| 4646b7d0ec | |||
| a02d51e504 | |||
| 9728c49edd | |||
| ed5b01c78d | |||
| 000091cfb9 | |||
| 2774701895 | |||
| e238dd8510 | |||
| c9b570e469 | |||
| c09ff25360 | |||
| 56b565b512 | |||
| 046f0a5c20 | |||
| 66c4d8f118 | |||
| 7f0a92f224 | |||
| 0ca8ee17be | |||
| c765d7d9eb | |||
| 237106428b | |||
| 06e1e1ba76 | |||
| 9eb4c3e77d | |||
| f3d8351208 | |||
| a331020bf8 | |||
| 2e6d9c34c0 | |||
| 1e1409e651 | |||
| ceaa32fd00 | |||
| f466e697dd | |||
| 476057663b | |||
| b53da82204 | |||
| 3b5e8921d0 | |||
| 1b1d0597fe | |||
| dafed3096f | |||
| dfa73a3d7c | |||
| 2a24e1d7e8 | |||
| 5cd624c7ea | |||
| 34b12a0315 | |||
| e9e6064eb6 | |||
| 6b7712e35f | |||
| f772fec407 | |||
| 7306d8c513 | |||
| 06cbd1fce1 | |||
| 84aac40410 | |||
| e40a0fdc50 | |||
| e3677995b9 | |||
| 0468c25bf8 | |||
| bc097c7667 | |||
| ed7150fac1 | |||
| 9c355bcfb7 | |||
| 1b6d8d803b | |||
| 06081627e8 | |||
| dc1e12d6ed | |||
| cb02deb837 | |||
| 2f8b89c8a6 | |||
| 2da650610c | |||
| 980024c9d2 | |||
| 94786c738b | |||
| bba7d0c828 | |||
| a04b69d0fd | |||
| de02a00ce9 | |||
| 25803f371c | |||
| a4eb5c07e6 | |||
| bad11f13f5 | |||
| 02d52d63b9 | |||
| 2821e43cdd | |||
| adea440931 | |||
| bbef99c3c2 | |||
| 527c01e7dc | |||
| 2a5a67e63c | |||
| 15051a1bc2 | |||
| b7d45341bc | |||
| 1695c7cc81 | |||
| 3e467959c9 | |||
| fbec26fc31 | |||
| b3092691b7 | |||
| 5a440d934d | |||
| 96c5176984 | |||
| 433430118f | |||
| 4431f5601a | |||
| bf48aa03a2 | |||
| 6d3ea8df59 | |||
| e52a0fc9d4 | |||
| 15a76d2639 | |||
| 059c21c990 | |||
| d833623ebf | |||
| 8b855d7ee4 | |||
| 706cde4ffd | |||
| a39a7a276d | |||
| 0327334fcd | |||
| 3e0d4ebbd6 |
@@ -1,106 +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"
|
|
||||||
else
|
|
||||||
TAG="canary"
|
|
||||||
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"
|
|
||||||
else
|
|
||||||
TAG="canary"
|
|
||||||
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}
|
|
||||||
else
|
|
||||||
TAG="canary"
|
|
||||||
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
|
|
||||||
- build-arm64:
|
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
only:
|
|
||||||
- main
|
|
||||||
- canary
|
|
||||||
- combine-manifests:
|
|
||||||
requires:
|
|
||||||
- build-amd64
|
|
||||||
- build-arm64
|
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
only:
|
|
||||||
- main
|
|
||||||
- canary
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
pnpm commitlint --edit $1
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// Skip Husky install in production and CI
|
|
||||||
if (process.env.NODE_ENV === "production" || process.env.CI === "true") {
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
const husky = (await import("husky")).default;
|
|
||||||
console.log(husky());
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pnpm lint-staged
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: Create a bug report
|
description: Create a bug report
|
||||||
labels: ['bug']
|
labels: ["needs-triage🔍"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
@@ -11,18 +11,27 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: To Reproduce
|
label: To Reproduce
|
||||||
description: A step-by-step description of how to reproduce the issue, or a link to the reproducible repository.
|
description: |
|
||||||
|
A detailed, step-by-step description of how to reproduce the issue is required.
|
||||||
|
Please ensure your report includes clear instructions using numbered lists.
|
||||||
|
|
||||||
|
If possible, provide a link to a repository or project where the issue can be reproduced.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
1. Create a application
|
1. Create a application
|
||||||
2. Click X
|
2. Click X
|
||||||
3. Y will happen
|
3. Y will happen
|
||||||
|
|
||||||
|
Make sure to:
|
||||||
|
- Use numbered lists to outline steps clearly.
|
||||||
|
- Include all relevant commands and configurations.
|
||||||
|
- Provide a link to a reproducible repository if applicable.
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Current vs. Expected behavior
|
label: Current vs. Expected behavior
|
||||||
description: A clear and concise description of what the bug is, and what you expected to happen.
|
description: A clear and concise description of what the bug is, and what you expected to happen.
|
||||||
placeholder: 'Following the steps from the previous section, I expected A to happen, but I observed B instead'
|
placeholder: "Following the steps from the previous section, I expected A to happen, but I observed B instead"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@@ -45,12 +54,24 @@ body:
|
|||||||
label: Which area(s) are affected? (Select all that apply)
|
label: Which area(s) are affected? (Select all that apply)
|
||||||
multiple: true
|
multiple: true
|
||||||
options:
|
options:
|
||||||
- 'Installation'
|
- "Installation"
|
||||||
- 'Application'
|
- "Application"
|
||||||
- 'Databases'
|
- "Databases"
|
||||||
- 'Docker Compose'
|
- "Docker Compose"
|
||||||
- 'Traefik'
|
- "Traefik"
|
||||||
- 'Docker'
|
- "Docker"
|
||||||
|
- "Remote server"
|
||||||
|
- "Local Development"
|
||||||
|
- "Cloud Version"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Are you deploying the applications where Dokploy is installed or on a remote server?
|
||||||
|
options:
|
||||||
|
- "Same server where Dokploy is installed"
|
||||||
|
- "Remote server"
|
||||||
|
- "Both"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@@ -59,4 +80,16 @@ body:
|
|||||||
description: |
|
description: |
|
||||||
Any extra information that might help us investigate.
|
Any extra information that might help us investigate.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
I tested on a DigitalOcean VPS with Ubuntu 20.04 and Docker version 20.10.12.
|
I tested on a DigitalOcean VPS with Ubuntu 20.04 and Docker version 20.10.12.
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Will you send a PR to fix it?
|
||||||
|
description: Let us know if you are planning to submit a pull request to address this issue.
|
||||||
|
|
||||||
|
options:
|
||||||
|
- "Yes"
|
||||||
|
- "No"
|
||||||
|
- "Maybe, need help"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: Feature Request
|
name: Feature Request
|
||||||
description: Suggest a new feature or improvement to the project
|
description: Suggest a new feature or improvement to the project
|
||||||
labels: ['enhancement']
|
labels: ["enhancement"]
|
||||||
body:
|
body:
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
@@ -30,4 +30,15 @@ body:
|
|||||||
label: Additional context
|
label: Additional context
|
||||||
description: Add any other context or screenshots about the feature request here.
|
description: Add any other context or screenshots about the feature request here.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Will you send a PR to implement it?
|
||||||
|
description: Let us know if you are planning to submit a pull request to implement this feature.
|
||||||
|
options:
|
||||||
|
- "Yes"
|
||||||
|
- "No"
|
||||||
|
- "Maybe, need help"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
@@ -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 }}
|
||||||
@@ -2,7 +2,7 @@ name: Build Docker images
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ["canary", "main"]
|
branches: ["canary", "main", "feat/monitoring"]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push-cloud-image:
|
build-and-push-cloud-image:
|
||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
@@ -53,8 +53,7 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
siumauricio/schedule:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
|
siumauricio/schedule:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
|
|
||||||
|
|
||||||
build-and-push-server-image:
|
build-and-push-server-image:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -77,4 +76,4 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
siumauricio/server:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
|
siumauricio/server:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
|
|||||||
@@ -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 }}
|
||||||
@@ -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
|
||||||
@@ -4,9 +4,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, canary]
|
branches: [main, canary]
|
||||||
|
|
||||||
env:
|
|
||||||
HUSKY: 0
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint-and-typecheck:
|
lint-and-typecheck:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -15,11 +12,10 @@ jobs:
|
|||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.18.0
|
node-version: 20.9.0
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm run server:build
|
- run: pnpm run server:build
|
||||||
- run: pnpm biome ci
|
|
||||||
- run: pnpm typecheck
|
- run: pnpm typecheck
|
||||||
|
|
||||||
build-and-test:
|
build-and-test:
|
||||||
@@ -30,7 +26,7 @@ jobs:
|
|||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.18.0
|
node-version: 20.9.0
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm run server:build
|
- run: pnpm run server:build
|
||||||
@@ -43,8 +39,8 @@ jobs:
|
|||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.18.0
|
node-version: 20.9.0
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm run server:build
|
- run: pnpm run server:build
|
||||||
- run: pnpm test
|
- run: pnpm test
|
||||||
|
|||||||
+3
-1
@@ -34,9 +34,11 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
# Editor
|
# Editor
|
||||||
.vscode
|
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
*.pem
|
||||||
|
|
||||||
|
|
||||||
|
.db
|
||||||
@@ -1 +0,0 @@
|
|||||||
npx commitlint --edit "$1"
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// Skip Husky install in production and CI
|
|
||||||
if (process.env.NODE_ENV === "production" || process.env.CI === "true") {
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
const husky = (await import("husky")).default;
|
|
||||||
console.log(husky());
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
pnpm run check
|
|
||||||
git add .
|
|
||||||
+17
-7
@@ -52,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.
|
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
|
```bash
|
||||||
git clone https://github.com/dokploy/dokploy.git
|
git clone https://github.com/dokploy/dokploy.git
|
||||||
cd dokploy
|
cd dokploy
|
||||||
@@ -71,9 +73,10 @@ Run the command that will spin up all the required services and files.
|
|||||||
pnpm run dokploy:setup
|
pnpm run dokploy:setup
|
||||||
```
|
```
|
||||||
|
|
||||||
Run this script
|
Run this script
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm run server:script
|
pnpm run server:script
|
||||||
```
|
```
|
||||||
|
|
||||||
Now run the development server.
|
Now run the development server.
|
||||||
@@ -135,11 +138,18 @@ curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
|||||||
&& ./install.sh
|
&& ./install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Railpack
|
||||||
|
curl -sSL https://railpack.com/install.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install Buildpacks
|
# Install Buildpacks
|
||||||
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Pull Request
|
## Pull Request
|
||||||
|
|
||||||
- The `main` branch is the source of truth and should always reflect the latest stable release.
|
- The `main` branch is the source of truth and should always reflect the latest stable release.
|
||||||
@@ -167,6 +177,7 @@ Let's take the example of `plausible` template.
|
|||||||
```typescript
|
```typescript
|
||||||
// EXAMPLE
|
// EXAMPLE
|
||||||
import {
|
import {
|
||||||
|
generateBase64,
|
||||||
generateHash,
|
generateHash,
|
||||||
generateRandomDomain,
|
generateRandomDomain,
|
||||||
type Template,
|
type Template,
|
||||||
@@ -198,8 +209,8 @@ export function generate(schema: Schema): Template {
|
|||||||
|
|
||||||
const mounts: Template["mounts"] = [
|
const mounts: Template["mounts"] = [
|
||||||
{
|
{
|
||||||
mountPath: "./clickhouse/clickhouse-config.xml",
|
filePath: "./clickhouse/clickhouse-config.xml",
|
||||||
content: `some content......`,
|
content: "some content......",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -235,14 +246,13 @@ export function generate(schema: Schema): Template {
|
|||||||
|
|
||||||
5. Add the logo or image of the template to `public/templates/plausible.svg`
|
5. Add the logo or image of the template to `public/templates/plausible.svg`
|
||||||
|
|
||||||
### Recomendations
|
### Recommendations
|
||||||
|
|
||||||
- Use the same name of the folder as the id of the template.
|
- Use the same name of the folder as the id of the template.
|
||||||
- The logo should be in the public folder.
|
- The logo should be in the public folder.
|
||||||
- If you want to show a domain in the UI, please add the prefix \_HOST at the end of the variable name.
|
- If you want to show a domain in the UI, please add the `_HOST` suffix at the end of the variable name.
|
||||||
- Test first on a vps or a server to make sure the template works.
|
- Test first on a vps or a server to make sure the template works.
|
||||||
|
|
||||||
## Docs & Website
|
## Docs & Website
|
||||||
|
|
||||||
To contribute to the Dokploy docs or website, please go to this [repository](https://github.com/Dokploy/website).
|
To contribute to the Dokploy docs or website, please go to this [repository](https://github.com/Dokploy/website).
|
||||||
|
|
||||||
|
|||||||
+9
-3
@@ -1,4 +1,4 @@
|
|||||||
FROM node:18-slim AS base
|
FROM node:20.9-slim AS base
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
@@ -7,7 +7,7 @@ FROM base AS build
|
|||||||
COPY . /usr/src/app
|
COPY . /usr/src/app
|
||||||
WORKDIR /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
|
# Install dependencies
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||||
@@ -29,7 +29,7 @@ WORKDIR /app
|
|||||||
# Set production
|
# Set production
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y curl unzip 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 only the necessary files
|
||||||
COPY --from=build /prod/dokploy/.next ./.next
|
COPY --from=build /prod/dokploy/.next ./.next
|
||||||
@@ -48,11 +48,17 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm
|
|||||||
|
|
||||||
# Install Nixpacks and tsx
|
# Install Nixpacks and tsx
|
||||||
# | VERBOSE=1 VERSION=1.21.0 bash
|
# | VERBOSE=1 VERSION=1.21.0 bash
|
||||||
|
|
||||||
|
ARG NIXPACKS_VERSION=1.29.1
|
||||||
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
||||||
&& chmod +x install.sh \
|
&& chmod +x install.sh \
|
||||||
&& ./install.sh \
|
&& ./install.sh \
|
||||||
&& pnpm install -g tsx
|
&& pnpm install -g tsx
|
||||||
|
|
||||||
|
# Install Railpack
|
||||||
|
ARG RAILPACK_VERSION=0.0.37
|
||||||
|
RUN curl -sSL https://railpack.com/install.sh | bash
|
||||||
|
|
||||||
# Install buildpacks
|
# Install buildpacks
|
||||||
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
|
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
FROM node:18-slim AS base
|
FROM node:20.9-slim AS base
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
@@ -7,7 +7,7 @@ FROM base AS build
|
|||||||
COPY . /usr/src/app
|
COPY . /usr/src/app
|
||||||
WORKDIR /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
|
# Install dependencies
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/dokploy install --frozen-lockfile
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/dokploy install --frozen-lockfile
|
||||||
|
|||||||
@@ -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"]
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
FROM node:18-slim AS base
|
FROM node:20.9-slim AS base
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
@@ -7,7 +7,7 @@ FROM base AS build
|
|||||||
COPY . /usr/src/app
|
COPY . /usr/src/app
|
||||||
WORKDIR /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
|
# Install dependencies
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/schedules install --frozen-lockfile
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/schedules install --frozen-lockfile
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
FROM node:18-slim AS base
|
FROM node:20.9-slim AS base
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
@@ -7,7 +7,7 @@ FROM base AS build
|
|||||||
COPY . /usr/src/app
|
COPY . /usr/src/app
|
||||||
WORKDIR /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
|
# Install dependencies
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/api install --frozen-lockfile
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/api install --frozen-lockfile
|
||||||
|
|||||||
+4
-4
@@ -17,10 +17,10 @@ See the License for the specific language governing permissions and limitations
|
|||||||
|
|
||||||
## Additional Terms for Specific Features
|
## Additional Terms for Specific Features
|
||||||
|
|
||||||
The following additional terms apply to the multi-node support, Docker Compose file and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
The following additional terms apply to the multi-node support, Docker Compose file, Preview Deployments and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
||||||
|
|
||||||
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support and Multi Server, will always be free to use in the self-hosted version.
|
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Preview Deployments and Multi Server, will always be free to use in the self-hosted version.
|
||||||
- **Restriction on Resale**: The multi-node support, Docker Compose file support and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
- **Restriction on Resale**: The multi-node support, Docker Compose file support, Preview Deployments and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
||||||
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support and Multi Server features must be distributed freely and cannot be sold or offered as a service.
|
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support, Preview Deployments and Multi Server features must be distributed freely and cannot be sold or offered as a service.
|
||||||
|
|
||||||
For further inquiries or permissions, please contact us directly.
|
For further inquiries or permissions, please contact us directly.
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ Dokploy includes multiple features to make your life easier.
|
|||||||
|
|
||||||
To get started, run the following command on a VPS:
|
To get started, run the following command on a VPS:
|
||||||
|
|
||||||
|
Want to skip the installation process? [Try the Dokploy Cloud](https://app.dokploy.com).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sSL https://dokploy.com/install.sh | sh
|
curl -sSL https://dokploy.com/install.sh | sh
|
||||||
```
|
```
|
||||||
@@ -60,12 +62,20 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
### Hero Sponsors 🎖
|
### Hero Sponsors 🎖
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; gap: 20px;">
|
<div style="display: flex; align-items: center; gap: 20px;">
|
||||||
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy" target="_blank" style="display: inline-block;">
|
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 10px;">
|
||||||
<img src=".github/sponsors/hostinger.jpg" alt="Hostinger" height="50"/>
|
<img src=".github/sponsors/hostinger.jpg" alt="Hostinger" height="50"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.lxaer.com/?ref=dokploy" target="_blank" style="display: inline-block;">
|
<a href="https://www.lxaer.com/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 10px;">
|
||||||
<img src=".github/sponsors/lxaer.png" alt="LX Aer" height="50"/>
|
<img src=".github/sponsors/lxaer.png" alt="LX Aer" height="50"/>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://mandarin3d.com/?ref=dokploy" target="_blank" style="display: inline-block;">
|
||||||
|
<img src=".github/sponsors/mandarin.png" alt="Mandarin" height="50"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://lightnode.com/?ref=dokploy" target="_blank" style="display: inline-block;">
|
||||||
|
<img src=".github/sponsors/light-node.webp" alt="Lightnode" height="70"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### Premium Supporters 🥇
|
### Premium Supporters 🥇
|
||||||
@@ -82,9 +92,14 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
|
|
||||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||||
<a href="https://lightspeed.run/?ref=dokploy"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
|
<a href="https://lightspeed.run/?ref=dokploy"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
|
||||||
<a href="https://cloudblast.io/?ref=dokploy "><img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" width="250px" alt="Lightspeed.run"/></a>
|
<a href="https://cloudblast.io/?ref=dokploy "><img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" width="250px" alt="Cloudblast.io"/></a>
|
||||||
|
<a href="https://startupfa.me/?ref=dokploy "><img src=".github/sponsors/startupfame.png" width="65px" alt="Startupfame"/></a>
|
||||||
|
<a href="https://itsdb-center.com?ref=dokploy "><img src=".github/sponsors/its.png" width="65px" alt="Itsdb-center"/></a>
|
||||||
|
<a href="https://openalternative.co/?ref=dokploy "><img src=".github/sponsors/openalternative.png" width="65px" alt="Openalternative"/></a>
|
||||||
|
<a href="https://synexa.ai/?ref=dokploy"><img src=".github/sponsors/synexa.png" width="65px" alt="Synexa"/></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
### Community Backers 🤝
|
### Community Backers 🤝
|
||||||
|
|
||||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||||
@@ -111,7 +126,7 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
## Video Tutorial
|
## Video Tutorial
|
||||||
|
|
||||||
<a href="https://youtu.be/mznYKPvhcfw">
|
<a href="https://youtu.be/mznYKPvhcfw">
|
||||||
<img src="https://dokploy.com/banner.webp" alt="Watch the video" width="400" style="border-radius:20px;"/>
|
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400" style="border-radius:20px;"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- ## Supported OS
|
<!-- ## Supported OS
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "PORT=4000 tsx watch src/index.ts",
|
"dev": "PORT=4000 tsx watch src/index.ts",
|
||||||
"build": "tsc --project tsconfig.json",
|
"build": "tsc --project tsconfig.json",
|
||||||
"start": "node --experimental-specifier-resolution=node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import "dotenv/config";
|
|||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { Queue } from "@nerimity/mimiqueue";
|
import { Queue } from "@nerimity/mimiqueue";
|
||||||
import { createClient } from "redis";
|
import { createClient } from "redis";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger.js";
|
||||||
import { type DeployJob, deployJobSchema } from "./schema";
|
import { type DeployJob, deployJobSchema } from "./schema.js";
|
||||||
import { deploy } from "./utils";
|
import { deploy } from "./utils.js";
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
const redisClient = createClient({
|
const redisClient = createClient({
|
||||||
@@ -28,7 +28,7 @@ app.use(async (c, next) => {
|
|||||||
|
|
||||||
app.post("/deploy", zValidator("json", deployJobSchema), (c) => {
|
app.post("/deploy", zValidator("json", deployJobSchema), (c) => {
|
||||||
const data = c.req.valid("json");
|
const data = c.req.valid("json");
|
||||||
const res = queue.add(data, { groupName: data.serverId });
|
queue.add(data, { groupName: data.serverId });
|
||||||
return c.json(
|
return c.json(
|
||||||
{
|
{
|
||||||
message: "Deployment Added",
|
message: "Deployment Added",
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
|||||||
applicationType: z.literal("compose"),
|
applicationType: z.literal("compose"),
|
||||||
serverId: z.string().min(1),
|
serverId: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
|
z.object({
|
||||||
|
applicationId: z.string(),
|
||||||
|
previewDeploymentId: z.string(),
|
||||||
|
titleLog: z.string(),
|
||||||
|
descriptionLog: z.string(),
|
||||||
|
server: z.boolean().optional(),
|
||||||
|
type: z.enum(["deploy"]),
|
||||||
|
applicationType: z.literal("application-preview"),
|
||||||
|
serverId: z.string().min(1),
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type DeployJob = z.infer<typeof deployJobSchema>;
|
export type DeployJob = z.infer<typeof deployJobSchema>;
|
||||||
|
|||||||
+21
-1
@@ -1,10 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
deployRemoteApplication,
|
deployRemoteApplication,
|
||||||
deployRemoteCompose,
|
deployRemoteCompose,
|
||||||
|
deployRemotePreviewApplication,
|
||||||
rebuildRemoteApplication,
|
rebuildRemoteApplication,
|
||||||
rebuildRemoteCompose,
|
rebuildRemoteCompose,
|
||||||
updateApplicationStatus,
|
updateApplicationStatus,
|
||||||
updateCompose,
|
updateCompose,
|
||||||
|
updatePreviewDeployment,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import type { DeployJob } from "./schema";
|
import type { DeployJob } from "./schema";
|
||||||
|
|
||||||
@@ -47,14 +49,32 @@ export const deploy = async (job: DeployJob) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (job.applicationType === "application-preview") {
|
||||||
|
await updatePreviewDeployment(job.previewDeploymentId, {
|
||||||
|
previewStatus: "running",
|
||||||
|
});
|
||||||
|
if (job.server) {
|
||||||
|
if (job.type === "deploy") {
|
||||||
|
await deployRemotePreviewApplication({
|
||||||
|
applicationId: job.applicationId,
|
||||||
|
titleLog: job.titleLog,
|
||||||
|
descriptionLog: job.descriptionLog,
|
||||||
|
previewDeploymentId: job.previewDeploymentId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (_) {
|
||||||
if (job.applicationType === "application") {
|
if (job.applicationType === "application") {
|
||||||
await updateApplicationStatus(job.applicationId, "error");
|
await updateApplicationStatus(job.applicationId, "error");
|
||||||
} else if (job.applicationType === "compose") {
|
} else if (job.applicationType === "compose") {
|
||||||
await updateCompose(job.composeId, {
|
await updateCompose(job.composeId, {
|
||||||
composeStatus: "error",
|
composeStatus: "error",
|
||||||
});
|
});
|
||||||
|
} else if (job.applicationType === "application-preview") {
|
||||||
|
await updatePreviewDeployment(job.previewDeploymentId, {
|
||||||
|
previewStatus: "error",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
18.18.0
|
20.9.0
|
||||||
@@ -17,10 +17,10 @@ See the License for the specific language governing permissions and limitations
|
|||||||
|
|
||||||
## Additional Terms for Specific Features
|
## Additional Terms for Specific Features
|
||||||
|
|
||||||
The following additional terms apply to the multi-node support, Docker Compose file and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
The following additional terms apply to the multi-node support, Docker Compose file, Preview Deployments and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
||||||
|
|
||||||
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support and Multi Server, will always be free to use in the self-hosted version.
|
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Preview Deployments and Multi Server, will always be free to use in the self-hosted version.
|
||||||
- **Restriction on Resale**: The multi-node support, Docker Compose file support and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
- **Restriction on Resale**: The multi-node support, Docker Compose file support, Preview Deployments and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
||||||
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support and Multi Server features must be distributed freely and cannot be sold or offered as a service.
|
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support, Preview Deployments and Multi Server features must be distributed freely and cannot be sold or offered as a service.
|
||||||
|
|
||||||
For further inquiries or permissions, please contact us directly.
|
For further inquiries or permissions, please contact us directly.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import { addSuffixToAllConfigs, addSuffixToConfigsRoot } from "@dokploy/server";
|
import { addSuffixToAllConfigs } from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ describe("createDomainLabels", () => {
|
|||||||
domainId: "",
|
domainId: "",
|
||||||
path: "/",
|
path: "/",
|
||||||
createdAt: "",
|
createdAt: "",
|
||||||
|
previewDeploymentId: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
it("should create basic labels for web entrypoint", async () => {
|
it("should create basic labels for web entrypoint", async () => {
|
||||||
@@ -39,6 +40,24 @@ describe("createDomainLabels", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should add the path prefix if is different than / empty", async () => {
|
||||||
|
const labels = await createDomainLabels(
|
||||||
|
appName,
|
||||||
|
{
|
||||||
|
...baseDomain,
|
||||||
|
path: "/hello",
|
||||||
|
},
|
||||||
|
"websecure",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(labels).toEqual([
|
||||||
|
"traefik.http.routers.test-app-1-websecure.rule=Host(`example.com`) && PathPrefix(`/hello`)",
|
||||||
|
"traefik.http.routers.test-app-1-websecure.entrypoints=websecure",
|
||||||
|
"traefik.http.services.test-app-1-websecure.loadbalancer.server.port=8080",
|
||||||
|
"traefik.http.routers.test-app-1-websecure.service=test-app-1-websecure",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("should add redirect middleware for https on web entrypoint", async () => {
|
it("should add redirect middleware for https on web entrypoint", async () => {
|
||||||
const httpsBaseDomain = { ...baseDomain, https: true };
|
const httpsBaseDomain = { ...baseDomain, https: true };
|
||||||
const labels = await createDomainLabels(appName, httpsBaseDomain, "web");
|
const labels = await createDomainLabels(appName, httpsBaseDomain, "web");
|
||||||
|
|||||||
@@ -293,29 +293,6 @@ networks:
|
|||||||
dokploy-network:
|
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", () => {
|
test("It shoudn't add suffix to dokploy-network", () => {
|
||||||
const composeData = load(composeFile7) as ComposeSpecification;
|
const composeData = load(composeFile7) as ComposeSpecification;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import { addSuffixToSecretsRoot } from "@dokploy/server";
|
import { addSuffixToSecretsRoot } from "@dokploy/server";
|
||||||
import type { ComposeSpecification } 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";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
test("Generate random hash with 8 characters", () => {
|
test("Generate random hash with 8 characters", () => {
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { addSuffixToAllVolumes } from "@dokploy/server";
|
||||||
import {
|
|
||||||
addSuffixToAllVolumes,
|
|
||||||
addSuffixToVolumesInServices,
|
|
||||||
} from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|||||||
@@ -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",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -26,12 +26,31 @@ if (typeof window === "undefined") {
|
|||||||
|
|
||||||
const baseApp: ApplicationNested = {
|
const baseApp: ApplicationNested = {
|
||||||
applicationId: "",
|
applicationId: "",
|
||||||
|
herokuVersion: "",
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
appName: "",
|
appName: "",
|
||||||
autoDeploy: true,
|
autoDeploy: true,
|
||||||
serverId: "",
|
serverId: "",
|
||||||
|
registryUrl: "",
|
||||||
branch: null,
|
branch: null,
|
||||||
dockerBuildStage: "",
|
dockerBuildStage: "",
|
||||||
|
isPreviewDeploymentsActive: false,
|
||||||
|
previewBuildArgs: null,
|
||||||
|
previewCertificateType: "none",
|
||||||
|
previewEnv: null,
|
||||||
|
previewHttps: false,
|
||||||
|
previewPath: "/",
|
||||||
|
previewPort: 3000,
|
||||||
|
previewLimit: 0,
|
||||||
|
previewWildcard: "",
|
||||||
|
project: {
|
||||||
|
env: "",
|
||||||
|
organizationId: "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
createdAt: "",
|
||||||
|
projectId: "",
|
||||||
|
},
|
||||||
buildArgs: null,
|
buildArgs: null,
|
||||||
buildPath: "/",
|
buildPath: "/",
|
||||||
gitlabPathNamespace: "",
|
gitlabPathNamespace: "",
|
||||||
|
|||||||
+179
@@ -0,0 +1,179 @@
|
|||||||
|
import { prepareEnvironmentVariables } from "@dokploy/server/index";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const projectEnv = `
|
||||||
|
ENVIRONMENT=staging
|
||||||
|
DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db
|
||||||
|
PORT=3000
|
||||||
|
`;
|
||||||
|
const serviceEnv = `
|
||||||
|
ENVIRONMENT=\${{project.ENVIRONMENT}}
|
||||||
|
DATABASE_URL=\${{project.DATABASE_URL}}
|
||||||
|
SERVICE_PORT=4000
|
||||||
|
`;
|
||||||
|
|
||||||
|
describe("prepareEnvironmentVariables", () => {
|
||||||
|
it("resolves project variables correctly", () => {
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"ENVIRONMENT=staging",
|
||||||
|
"DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db",
|
||||||
|
"SERVICE_PORT=4000",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles undefined project variables", () => {
|
||||||
|
const incompleteProjectEnv = `
|
||||||
|
NODE_ENV=production
|
||||||
|
`;
|
||||||
|
|
||||||
|
const invalidServiceEnv = `
|
||||||
|
UNDEFINED_VAR=\${{project.UNDEFINED_VAR}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() =>
|
||||||
|
prepareEnvironmentVariables(invalidServiceEnv, incompleteProjectEnv), // Cambiado el orden
|
||||||
|
).toThrow("Invalid project environment variable: project.UNDEFINED_VAR");
|
||||||
|
});
|
||||||
|
it("allows service-specific variables to override project variables", () => {
|
||||||
|
const serviceSpecificEnv = `
|
||||||
|
ENVIRONMENT=production
|
||||||
|
DATABASE_URL=\${{project.DATABASE_URL}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariables(
|
||||||
|
serviceSpecificEnv,
|
||||||
|
projectEnv,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"ENVIRONMENT=production", // Overrides project variable
|
||||||
|
"DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves complex references for dynamic endpoints", () => {
|
||||||
|
const projectEnv = `
|
||||||
|
BASE_URL=https://api.example.com
|
||||||
|
API_VERSION=v1
|
||||||
|
PORT=8000
|
||||||
|
`;
|
||||||
|
const serviceEnv = `
|
||||||
|
API_ENDPOINT=\${{project.BASE_URL}}/\${{project.API_VERSION}}/endpoint
|
||||||
|
SERVICE_PORT=9000
|
||||||
|
`;
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"API_ENDPOINT=https://api.example.com/v1/endpoint",
|
||||||
|
"SERVICE_PORT=9000",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles missing project variables gracefully", () => {
|
||||||
|
const projectEnv = `
|
||||||
|
PORT=8080
|
||||||
|
`;
|
||||||
|
const serviceEnv = `
|
||||||
|
MISSING_VAR=\${{project.MISSING_KEY}}
|
||||||
|
SERVICE_PORT=3000
|
||||||
|
`;
|
||||||
|
|
||||||
|
expect(() => prepareEnvironmentVariables(serviceEnv, projectEnv)).toThrow(
|
||||||
|
"Invalid project environment variable: project.MISSING_KEY",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("overrides project variables with service-specific values", () => {
|
||||||
|
const projectEnv = `
|
||||||
|
ENVIRONMENT=staging
|
||||||
|
DATABASE_URL=postgres://project:project@localhost:5432/project_db
|
||||||
|
`;
|
||||||
|
const serviceEnv = `
|
||||||
|
ENVIRONMENT=\${{project.ENVIRONMENT}}
|
||||||
|
DATABASE_URL=postgres://service:service@localhost:5432/service_db
|
||||||
|
SERVICE_NAME=my-service
|
||||||
|
`;
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"ENVIRONMENT=staging",
|
||||||
|
"DATABASE_URL=postgres://service:service@localhost:5432/service_db",
|
||||||
|
"SERVICE_NAME=my-service",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles project variables with normal and unusual characters", () => {
|
||||||
|
const projectEnv = `
|
||||||
|
ENVIRONMENT=PRODUCTION
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Needs to be in quotes
|
||||||
|
const serviceEnv = `
|
||||||
|
NODE_ENV=\${{project.ENVIRONMENT}}
|
||||||
|
SPECIAL_VAR="$^@$^@#$^@!#$@#$-\${{project.ENVIRONMENT}}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"NODE_ENV=PRODUCTION",
|
||||||
|
"SPECIAL_VAR=$^@$^@#$^@!#$@#$-PRODUCTION",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles complex cases with multiple references, special characters, and spaces", () => {
|
||||||
|
const projectEnv = `
|
||||||
|
ENVIRONMENT=STAGING
|
||||||
|
APP_NAME=MyApp
|
||||||
|
`;
|
||||||
|
|
||||||
|
const serviceEnv = `
|
||||||
|
NODE_ENV=\${{project.ENVIRONMENT}}
|
||||||
|
COMPLEX_VAR="Prefix-$#^!@-\${{project.ENVIRONMENT}}--\${{project.APP_NAME}} Suffix "
|
||||||
|
`;
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"NODE_ENV=STAGING",
|
||||||
|
"COMPLEX_VAR=Prefix-$#^!@-STAGING--MyApp Suffix ",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles references enclosed in single quotes", () => {
|
||||||
|
const projectEnv = `
|
||||||
|
ENVIRONMENT=STAGING
|
||||||
|
APP_NAME=MyApp
|
||||||
|
`;
|
||||||
|
|
||||||
|
const serviceEnv = `
|
||||||
|
NODE_ENV='\${{project.ENVIRONMENT}}'
|
||||||
|
COMPLEX_VAR='Prefix-$#^!@-\${{project.ENVIRONMENT}}--\${{project.APP_NAME}} Suffix'
|
||||||
|
`;
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"NODE_ENV=STAGING",
|
||||||
|
"COMPLEX_VAR=Prefix-$#^!@-STAGING--MyApp Suffix",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles double and single quotes combined", () => {
|
||||||
|
const projectEnv = `
|
||||||
|
ENVIRONMENT=PRODUCTION
|
||||||
|
APP_NAME=MyApp
|
||||||
|
`;
|
||||||
|
const serviceEnv = `
|
||||||
|
NODE_ENV="'\${{project.ENVIRONMENT}}'"
|
||||||
|
COMPLEX_VAR="'Prefix \"DoubleQuoted\" and \${{project.APP_NAME}}'"
|
||||||
|
`;
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"NODE_ENV='PRODUCTION'",
|
||||||
|
"COMPLEX_VAR='Prefix \"DoubleQuoted\" and MyApp'",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,7 +5,7 @@ vi.mock("node:fs", () => ({
|
|||||||
default: fs,
|
default: fs,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import type { Admin, FileConfig } from "@dokploy/server";
|
import type { FileConfig, User } from "@dokploy/server";
|
||||||
import {
|
import {
|
||||||
createDefaultServerTraefikConfig,
|
createDefaultServerTraefikConfig,
|
||||||
loadOrCreateConfig,
|
loadOrCreateConfig,
|
||||||
@@ -13,20 +13,57 @@ import {
|
|||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { beforeEach, expect, test, vi } from "vitest";
|
import { beforeEach, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
const baseAdmin: Admin = {
|
const baseAdmin: User = {
|
||||||
createdAt: "",
|
enablePaidFeatures: false,
|
||||||
authId: "",
|
metricsConfig: {
|
||||||
adminId: "string",
|
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,
|
serverIp: null,
|
||||||
certificateType: "none",
|
certificateType: "none",
|
||||||
host: null,
|
host: null,
|
||||||
letsEncryptEmail: null,
|
letsEncryptEmail: null,
|
||||||
sshPrivateKey: null,
|
sshPrivateKey: null,
|
||||||
enableDockerCleanup: false,
|
enableDockerCleanup: false,
|
||||||
enableLogRotation: false,
|
logCleanupCron: null,
|
||||||
serversQuantity: 0,
|
serversQuantity: 0,
|
||||||
stripeCustomerId: "",
|
stripeCustomerId: "",
|
||||||
stripeSubscriptionId: "",
|
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(() => {
|
beforeEach(() => {
|
||||||
@@ -77,8 +114,6 @@ test("Should not touch config without host", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Should remove websecure if https rollback to http", () => {
|
test("Should remove websecure if https rollback to http", () => {
|
||||||
const originalConfig: FileConfig = loadOrCreateConfig("dokploy");
|
|
||||||
|
|
||||||
updateServerTraefik(
|
updateServerTraefik(
|
||||||
{ ...baseAdmin, certificateType: "letsencrypt" },
|
{ ...baseAdmin, certificateType: "letsencrypt" },
|
||||||
"example.com",
|
"example.com",
|
||||||
|
|||||||
@@ -6,13 +6,32 @@ import { expect, test } from "vitest";
|
|||||||
|
|
||||||
const baseApp: ApplicationNested = {
|
const baseApp: ApplicationNested = {
|
||||||
applicationId: "",
|
applicationId: "",
|
||||||
|
herokuVersion: "",
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
appName: "",
|
appName: "",
|
||||||
autoDeploy: true,
|
autoDeploy: true,
|
||||||
serverId: "",
|
serverId: "",
|
||||||
branch: null,
|
branch: null,
|
||||||
dockerBuildStage: "",
|
dockerBuildStage: "",
|
||||||
|
registryUrl: "",
|
||||||
buildArgs: null,
|
buildArgs: null,
|
||||||
|
isPreviewDeploymentsActive: false,
|
||||||
|
previewBuildArgs: null,
|
||||||
|
previewCertificateType: "none",
|
||||||
|
previewEnv: null,
|
||||||
|
previewHttps: false,
|
||||||
|
previewPath: "/",
|
||||||
|
previewPort: 3000,
|
||||||
|
previewLimit: 0,
|
||||||
|
previewWildcard: "",
|
||||||
|
project: {
|
||||||
|
env: "",
|
||||||
|
organizationId: "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
createdAt: "",
|
||||||
|
projectId: "",
|
||||||
|
},
|
||||||
buildPath: "/",
|
buildPath: "/",
|
||||||
gitlabPathNamespace: "",
|
gitlabPathNamespace: "",
|
||||||
buildType: "nixpacks",
|
buildType: "nixpacks",
|
||||||
@@ -86,6 +105,7 @@ const baseDomain: Domain = {
|
|||||||
composeId: "",
|
composeId: "",
|
||||||
domainType: "application",
|
domainType: "application",
|
||||||
uniqueConfigKey: 1,
|
uniqueConfigKey: 1,
|
||||||
|
previewDeploymentId: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseRedirect: Redirect = {
|
const baseRedirect: Redirect = {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export default defineConfig({
|
|||||||
NODE: "test",
|
NODE: "test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
plugins: [tsconfigPaths()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@dokploy/server": path.resolve(
|
"@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 Setup</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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+2
-2
@@ -130,7 +130,7 @@ const createStringToJSONSchema = (schema: z.ZodTypeAny) => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return JSON.parse(str);
|
return JSON.parse(str);
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
ctx.addIssue({ code: "custom", message: "Invalid JSON format" });
|
ctx.addIssue({ code: "custom", message: "Invalid JSON format" });
|
||||||
return z.NEVER;
|
return z.NEVER;
|
||||||
}
|
}
|
||||||
@@ -259,7 +259,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
|||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the swarm settings");
|
toast.error("Error updating the swarm settings");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
|||||||
+1
-2
@@ -29,7 +29,6 @@ import { api } from "@/utils/api";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Server } from "lucide-react";
|
import { Server } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React from "react";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -94,7 +93,7 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the command");
|
toast.error("Error updating the command");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import React from "react";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -71,7 +70,7 @@ export const AddCommand = ({ applicationId }: Props) => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the command");
|
toast.error("Error updating the command");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,7 +80,8 @@ export const AddCommand = ({ applicationId }: Props) => {
|
|||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-xl">Run Command</CardTitle>
|
<CardTitle className="text-xl">Run Command</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Run a custom command in the container
|
Run a custom command in the container after the application
|
||||||
|
initialized
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</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 succesfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to delete the port");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+36
-11
@@ -27,7 +27,7 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -45,18 +45,29 @@ type AddPort = z.infer<typeof AddPortSchema>;
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
|
portId?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddPort = ({
|
export const HandlePorts = ({
|
||||||
applicationId,
|
applicationId,
|
||||||
|
portId,
|
||||||
children = <PlusIcon className="h-4 w-4" />,
|
children = <PlusIcon className="h-4 w-4" />,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { data } = api.port.one.useQuery(
|
||||||
api.port.create.useMutation();
|
{
|
||||||
|
portId: portId ?? "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!portId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const { mutateAsync, isLoading, error, isError } = portId
|
||||||
|
? api.port.update.useMutation()
|
||||||
|
: api.port.create.useMutation();
|
||||||
|
|
||||||
const form = useForm<AddPort>({
|
const form = useForm<AddPort>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -68,32 +79,46 @@ export const AddPort = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
publishedPort: 0,
|
publishedPort: data?.publishedPort ?? 0,
|
||||||
targetPort: 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) => {
|
const onSubmit = async (data: AddPort) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
applicationId,
|
applicationId,
|
||||||
...data,
|
...data,
|
||||||
|
portId: portId || "",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Port Created");
|
toast.success(portId ? "Port Updated" : "Port Created");
|
||||||
await utils.application.one.invalidate({
|
await utils.application.one.invalidate({
|
||||||
applicationId,
|
applicationId,
|
||||||
});
|
});
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to create the port");
|
toast.error(
|
||||||
|
portId ? "Error updating the port" : "Error creating the port",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<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>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -204,7 +229,7 @@ export const AddPort = ({
|
|||||||
form="hook-form-add-port"
|
form="hook-form-add-port"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Create
|
{portId ? "Update" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -7,23 +9,24 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Rss } from "lucide-react";
|
import { Rss, Trash2 } from "lucide-react";
|
||||||
import React from "react";
|
import { toast } from "sonner";
|
||||||
import { AddPort } from "./add-port";
|
import { HandlePorts } from "./handle-ports";
|
||||||
import { DeletePort } from "./delete-port";
|
|
||||||
import { UpdatePort } from "./update-port";
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowPorts = ({ applicationId }: Props) => {
|
export const ShowPorts = ({ applicationId }: Props) => {
|
||||||
const { data } = api.application.one.useQuery(
|
const { data, refetch } = api.application.one.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
},
|
},
|
||||||
{ enabled: !!applicationId },
|
{ enabled: !!applicationId },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { mutateAsync: deletePort, isLoading: isRemoving } =
|
||||||
|
api.port.delete.useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||||
@@ -35,7 +38,7 @@ export const ShowPorts = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data && data?.ports.length > 0 && (
|
{data && data?.ports.length > 0 && (
|
||||||
<AddPort applicationId={applicationId}>Add Port</AddPort>
|
<HandlePorts applicationId={applicationId}>Add Port</HandlePorts>
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
@@ -45,7 +48,7 @@ export const ShowPorts = ({ applicationId }: Props) => {
|
|||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No ports configured
|
No ports configured
|
||||||
</span>
|
</span>
|
||||||
<AddPort applicationId={applicationId}>Add Port</AddPort>
|
<HandlePorts applicationId={applicationId}>Add Port</HandlePorts>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pt-2 gap-4">
|
<div className="flex flex-col pt-2 gap-4">
|
||||||
@@ -78,8 +81,36 @@ export const ShowPorts = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<UpdatePort portId={port.portId} />
|
<HandlePorts
|
||||||
<DeletePort portId={port.portId} />
|
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>
|
</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 to update 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>
|
|
||||||
<Input 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 succesfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to delete the redirect");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+43
-14
@@ -31,7 +31,7 @@ import { Separator } from "@/components/ui/separator";
|
|||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -61,7 +61,7 @@ const redirectPresets = [
|
|||||||
redirect: {
|
redirect: {
|
||||||
regex: "^https?://(?:www.)?(.+)",
|
regex: "^https?://(?:www.)?(.+)",
|
||||||
permanent: true,
|
permanent: true,
|
||||||
replacement: "https://www.$${1}",
|
replacement: "https://www.${1}",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -70,26 +70,39 @@ const redirectPresets = [
|
|||||||
redirect: {
|
redirect: {
|
||||||
regex: "^https?://www.(.+)",
|
regex: "^https?://www.(.+)",
|
||||||
permanent: true,
|
permanent: true,
|
||||||
replacement: "https://$${1}",
|
replacement: "https://${1}",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
|
redirectId?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddRedirect = ({
|
export const HandleRedirect = ({
|
||||||
applicationId,
|
applicationId,
|
||||||
|
redirectId,
|
||||||
children = <PlusIcon className="w-4 h-4" />,
|
children = <PlusIcon className="w-4 h-4" />,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [presetSelected, setPresetSelected] = useState("");
|
const [presetSelected, setPresetSelected] = useState("");
|
||||||
|
|
||||||
|
const { data, refetch } = api.redirects.one.useQuery(
|
||||||
|
{
|
||||||
|
redirectId: redirectId || "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!redirectId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } = redirectId
|
||||||
api.redirects.create.useMutation();
|
? api.redirects.update.useMutation()
|
||||||
|
: api.redirects.create.useMutation();
|
||||||
|
|
||||||
const form = useForm<AddRedirect>({
|
const form = useForm<AddRedirect>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -102,29 +115,35 @@ export const AddRedirect = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
permanent: false,
|
permanent: data?.permanent || false,
|
||||||
regex: "",
|
regex: data?.regex || "",
|
||||||
replacement: "",
|
replacement: data?.replacement || "",
|
||||||
});
|
});
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||||
|
|
||||||
const onSubmit = async (data: AddRedirect) => {
|
const onSubmit = async (data: AddRedirect) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
applicationId,
|
applicationId,
|
||||||
...data,
|
...data,
|
||||||
|
redirectId: redirectId || "",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Redirect Created");
|
toast.success(redirectId ? "Redirect Updated" : "Redirect Created");
|
||||||
await utils.application.one.invalidate({
|
await utils.application.one.invalidate({
|
||||||
applicationId,
|
applicationId,
|
||||||
});
|
});
|
||||||
|
refetch();
|
||||||
await utils.application.readTraefikConfig.invalidate({
|
await utils.application.readTraefikConfig.invalidate({
|
||||||
applicationId,
|
applicationId,
|
||||||
});
|
});
|
||||||
onDialogToggle(false);
|
onDialogToggle(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to create the redirect");
|
toast.error(
|
||||||
|
redirectId
|
||||||
|
? "Error updating the redirect"
|
||||||
|
: "Error creating the redirect",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -148,7 +167,17 @@ export const AddRedirect = ({
|
|||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onDialogToggle}>
|
<Dialog open={isOpen} onOpenChange={onDialogToggle}>
|
||||||
<DialogTrigger asChild>
|
<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>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -243,7 +272,7 @@ export const AddRedirect = ({
|
|||||||
form="hook-form-add-redirect"
|
form="hook-form-add-redirect"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Create
|
{redirectId ? "Update" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Form>
|
</Form>
|
||||||
+51
-11
@@ -1,3 +1,5 @@
|
|||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -6,23 +8,27 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Split } from "lucide-react";
|
import { Split, Trash2 } from "lucide-react";
|
||||||
import React from "react";
|
import { toast } from "sonner";
|
||||||
import { AddRedirect } from "./add-redirect";
|
import { HandleRedirect } from "./handle-redirect";
|
||||||
import { DeleteRedirect } from "./delete-redirect";
|
|
||||||
import { UpdateRedirect } from "./update-redirect";
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowRedirects = ({ applicationId }: Props) => {
|
export const ShowRedirects = ({ applicationId }: Props) => {
|
||||||
const { data } = api.application.one.useQuery(
|
const { data, refetch } = api.application.one.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
},
|
},
|
||||||
{ enabled: !!applicationId },
|
{ enabled: !!applicationId },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { mutateAsync: deleteRedirect, isLoading: isRemoving } =
|
||||||
|
api.redirects.delete.useMutation();
|
||||||
|
|
||||||
|
const utils = api.useUtils();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||||
@@ -35,7 +41,9 @@ export const ShowRedirects = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data && data?.redirects.length > 0 && (
|
{data && data?.redirects.length > 0 && (
|
||||||
<AddRedirect applicationId={applicationId}>Add Redirect</AddRedirect>
|
<HandleRedirect applicationId={applicationId}>
|
||||||
|
Add Redirect
|
||||||
|
</HandleRedirect>
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
@@ -45,9 +53,9 @@ export const ShowRedirects = ({ applicationId }: Props) => {
|
|||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No redirects configured
|
No redirects configured
|
||||||
</span>
|
</span>
|
||||||
<AddRedirect applicationId={applicationId}>
|
<HandleRedirect applicationId={applicationId}>
|
||||||
Add Redirect
|
Add Redirect
|
||||||
</AddRedirect>
|
</HandleRedirect>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pt-2">
|
<div className="flex flex-col pt-2">
|
||||||
@@ -76,8 +84,40 @@ export const ShowRedirects = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<UpdateRedirect redirectId={redirect.redirectId} />
|
<HandleRedirect
|
||||||
<DeleteRedirect redirectId={redirect.redirectId} />
|
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>
|
</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 to update 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 succesfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to delete the security");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+41
-11
@@ -20,7 +20,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -35,17 +35,29 @@ type AddSecurity = z.infer<typeof AddSecuritychema>;
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
|
securityId?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddSecurity = ({
|
export const HandleSecurity = ({
|
||||||
applicationId,
|
applicationId,
|
||||||
|
securityId,
|
||||||
children = <PlusIcon className="h-4 w-4" />,
|
children = <PlusIcon className="h-4 w-4" />,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { data } = api.security.one.useQuery(
|
||||||
api.security.create.useMutation();
|
{
|
||||||
|
securityId: securityId ?? "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!securityId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading, error, isError } = securityId
|
||||||
|
? api.security.update.useMutation()
|
||||||
|
: api.security.create.useMutation();
|
||||||
|
|
||||||
const form = useForm<AddSecurity>({
|
const form = useForm<AddSecurity>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -56,16 +68,20 @@ export const AddSecurity = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset();
|
form.reset({
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
username: data?.username || "",
|
||||||
|
password: data?.password || "",
|
||||||
|
});
|
||||||
|
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||||
|
|
||||||
const onSubmit = async (data: AddSecurity) => {
|
const onSubmit = async (data: AddSecurity) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
applicationId,
|
applicationId,
|
||||||
...data,
|
...data,
|
||||||
|
securityId: securityId || "",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Security Created");
|
toast.success(securityId ? "Security Updated" : "Security Created");
|
||||||
await utils.application.one.invalidate({
|
await utils.application.one.invalidate({
|
||||||
applicationId,
|
applicationId,
|
||||||
});
|
});
|
||||||
@@ -75,20 +91,34 @@ export const AddSecurity = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to create the security");
|
toast.error(
|
||||||
|
securityId
|
||||||
|
? "Error updating the security"
|
||||||
|
: "Error creating security",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<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>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Security</DialogTitle>
|
<DialogTitle>Security</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Add security to your application
|
{securityId ? "Update" : "Add"} security to your application
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
@@ -137,7 +167,7 @@ export const AddSecurity = ({
|
|||||||
form="hook-form-add-security"
|
form="hook-form-add-security"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Create
|
{securityId ? "Update" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -6,23 +8,26 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { LockKeyhole } from "lucide-react";
|
import { LockKeyhole, Trash2 } from "lucide-react";
|
||||||
import React from "react";
|
import { toast } from "sonner";
|
||||||
import { AddSecurity } from "./add-security";
|
import { HandleSecurity } from "./handle-security";
|
||||||
import { DeleteSecurity } from "./delete-security";
|
|
||||||
import { UpdateSecurity } from "./update-security";
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowSecurity = ({ applicationId }: Props) => {
|
export const ShowSecurity = ({ applicationId }: Props) => {
|
||||||
const { data } = api.application.one.useQuery(
|
const { data, refetch } = api.application.one.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
},
|
},
|
||||||
{ enabled: !!applicationId },
|
{ enabled: !!applicationId },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { mutateAsync: deleteSecurity, isLoading: isRemoving } =
|
||||||
|
api.security.delete.useMutation();
|
||||||
|
|
||||||
|
const utils = api.useUtils();
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||||
@@ -32,7 +37,9 @@ export const ShowSecurity = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data && data?.security.length > 0 && (
|
{data && data?.security.length > 0 && (
|
||||||
<AddSecurity applicationId={applicationId}>Add Security</AddSecurity>
|
<HandleSecurity applicationId={applicationId}>
|
||||||
|
Add Security
|
||||||
|
</HandleSecurity>
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
@@ -42,9 +49,9 @@ export const ShowSecurity = ({ applicationId }: Props) => {
|
|||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No security configured
|
No security configured
|
||||||
</span>
|
</span>
|
||||||
<AddSecurity applicationId={applicationId}>
|
<HandleSecurity applicationId={applicationId}>
|
||||||
Add Security
|
Add Security
|
||||||
</AddSecurity>
|
</HandleSecurity>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pt-2">
|
<div className="flex flex-col pt-2">
|
||||||
@@ -67,8 +74,39 @@ export const ShowSecurity = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
<UpdateSecurity securityId={security.securityId} />
|
<HandleSecurity
|
||||||
<DeleteSecurity securityId={security.securityId} />
|
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>
|
</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 to update 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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
-231
@@ -1,231 +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";
|
|
||||||
|
|
||||||
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 to Update 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>
|
|
||||||
<FormLabel>Memory Reservation</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="256 MB"
|
|
||||||
{...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>
|
|
||||||
<FormLabel>Memory Limit</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder={"1024 MB"}
|
|
||||||
{...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>
|
|
||||||
<FormLabel>Cpu Limit</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder={"2"}
|
|
||||||
{...field}
|
|
||||||
type="number"
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (
|
|
||||||
value === "" ||
|
|
||||||
/^[0-9]*\.?[0-9]*$/.test(value)
|
|
||||||
) {
|
|
||||||
const float = Number.parseFloat(value);
|
|
||||||
field.onChange(float);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="cpuReservation"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Cpu Reservation</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder={"1"}
|
|
||||||
{...field}
|
|
||||||
type="number"
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (
|
|
||||||
value === "" ||
|
|
||||||
/^[0-9]*\.?[0-9]*$/.test(value)
|
|
||||||
) {
|
|
||||||
const float = Number.parseFloat(value);
|
|
||||||
field.onChange(float);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full justify-end">
|
|
||||||
<Button isLoading={isLoading} type="submit">
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
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 {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { InfoIcon } from "lucide-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const addResourcesSchema = z.object({
|
||||||
|
memoryReservation: z.string().optional(),
|
||||||
|
cpuLimit: z.string().optional(),
|
||||||
|
memoryLimit: z.string().optional(),
|
||||||
|
cpuReservation: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ServiceType =
|
||||||
|
| "postgres"
|
||||||
|
| "mongo"
|
||||||
|
| "redis"
|
||||||
|
| "mysql"
|
||||||
|
| "mariadb"
|
||||||
|
| "application";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
type: ServiceType | "application";
|
||||||
|
}
|
||||||
|
|
||||||
|
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: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(addResourcesSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
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: AddResources) => {
|
||||||
|
await mutateAsync({
|
||||||
|
mongoId: id || "",
|
||||||
|
postgresId: id || "",
|
||||||
|
redisId: id || "",
|
||||||
|
mysqlId: id || "",
|
||||||
|
mariadbId: id || "",
|
||||||
|
applicationId: id || "",
|
||||||
|
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="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}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<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}
|
||||||
|
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() || ""}
|
||||||
|
/>
|
||||||
|
</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} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full justify-end">
|
||||||
|
<Button isLoading={isLoading} type="submit">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { File, Loader2 } from "lucide-react";
|
import { File, Loader2 } from "lucide-react";
|
||||||
import React from "react";
|
|
||||||
import { UpdateTraefikConfig } from "./update-traefik-config";
|
import { UpdateTraefikConfig } from "./update-traefik-config";
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
|
|||||||
+1
-1
@@ -105,7 +105,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
|||||||
form.reset();
|
form.reset();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the traefik config");
|
toast.error("Error updating the Traefik config");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -18,7 +19,6 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -107,7 +107,7 @@ export const AddVolumes = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to create the Bind mount");
|
toast.error("Error creating the Bind mount");
|
||||||
});
|
});
|
||||||
} else if (data.type === "volume") {
|
} else if (data.type === "volume") {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -122,7 +122,7 @@ export const AddVolumes = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to create the Volume mount");
|
toast.error("Error creating the Volume mount");
|
||||||
});
|
});
|
||||||
} else if (data.type === "file") {
|
} else if (data.type === "file") {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -138,7 +138,7 @@ export const AddVolumes = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to create the File mount");
|
toast.error("Error creating the File mount");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ export const AddVolumes = ({
|
|||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
<Button>{children}</Button>
|
<Button>{children}</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-3xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Volumes / Mounts</DialogTitle>
|
<DialogTitle>Volumes / Mounts</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -303,9 +303,12 @@ export const AddVolumes = ({
|
|||||||
<FormLabel>Content</FormLabel>
|
<FormLabel>Content</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<CodeEditor
|
||||||
placeholder="Any content"
|
language="properties"
|
||||||
className="h-64"
|
placeholder={`NODE_ENV=production
|
||||||
|
PORT=3000
|
||||||
|
`}
|
||||||
|
className="h-96 font-mono"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -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 succesfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to delete the mount");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -7,40 +9,48 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Package } from "lucide-react";
|
import { Package, Trash2 } from "lucide-react";
|
||||||
import React from "react";
|
import { toast } from "sonner";
|
||||||
|
import type { ServiceType } from "../show-resources";
|
||||||
import { AddVolumes } from "./add-volumes";
|
import { AddVolumes } from "./add-volumes";
|
||||||
import { DeleteVolume } from "./delete-volume";
|
|
||||||
import { UpdateVolume } from "./update-volume";
|
import { UpdateVolume } from "./update-volume";
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
id: string;
|
||||||
|
type: ServiceType | "compose";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowVolumes = ({ applicationId }: Props) => {
|
export const ShowVolumes = ({ id, type }: Props) => {
|
||||||
const { data, refetch } = api.application.one.useQuery(
|
const queryMap = {
|
||||||
{
|
postgres: () =>
|
||||||
applicationId,
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
},
|
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||||
{ enabled: !!applicationId },
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
);
|
mariadb: () =>
|
||||||
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
|
application: () =>
|
||||||
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
|
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 (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-xl">Volumes</CardTitle>
|
<CardTitle className="text-xl">Volumes</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
If you want to persist data in this application use the following
|
If you want to persist data in this service use the following config
|
||||||
config to setup the volumes
|
to setup the volumes
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data && data?.mounts.length > 0 && (
|
{data && data?.mounts.length > 0 && (
|
||||||
<AddVolumes
|
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
||||||
serviceId={applicationId}
|
|
||||||
refetch={refetch}
|
|
||||||
serviceType="application"
|
|
||||||
>
|
|
||||||
Add Volume
|
Add Volume
|
||||||
</AddVolumes>
|
</AddVolumes>
|
||||||
)}
|
)}
|
||||||
@@ -52,17 +62,13 @@ export const ShowVolumes = ({ applicationId }: Props) => {
|
|||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No volumes/mounts configured
|
No volumes/mounts configured
|
||||||
</span>
|
</span>
|
||||||
<AddVolumes
|
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
||||||
serviceId={applicationId}
|
|
||||||
refetch={refetch}
|
|
||||||
serviceType="application"
|
|
||||||
>
|
|
||||||
Add Volume
|
Add Volume
|
||||||
</AddVolumes>
|
</AddVolumes>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pt-2 gap-4">
|
<div className="flex flex-col pt-2 gap-4">
|
||||||
<AlertBlock type="info">
|
<AlertBlock type="warning">
|
||||||
Please remember to click Redeploy after adding, editing, or
|
Please remember to click Redeploy after adding, editing, or
|
||||||
deleting a mount to apply the changes.
|
deleting a mount to apply the changes.
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
@@ -73,7 +79,8 @@ export const ShowVolumes = ({ applicationId }: Props) => {
|
|||||||
key={mount.mountId}
|
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"
|
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">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="font-medium">Mount Type</span>
|
<span className="font-medium">Mount Type</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
@@ -90,21 +97,12 @@ export const ShowVolumes = ({ applicationId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{mount.type === "file" && (
|
{mount.type === "file" && (
|
||||||
<>
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex flex-col gap-1">
|
<span className="font-medium">Content</span>
|
||||||
<span className="font-medium">Content</span>
|
<span className="text-sm text-muted-foreground line-clamp-[10] whitespace-break-spaces">
|
||||||
<span className="text-sm text-muted-foreground">
|
{mount.content}
|
||||||
{mount.content}
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</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" && (
|
{mount.type === "bind" && (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@@ -114,21 +112,55 @@ export const ShowVolumes = ({ applicationId }: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-1">
|
{mount.type === "file" ? (
|
||||||
<span className="font-medium">Mount Path</span>
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="font-medium">File Path</span>
|
||||||
{mount.mountPath}
|
<span className="text-sm text-muted-foreground">
|
||||||
</span>
|
{mount.filePath}
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="font-medium">Mount Path</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{mount.mountPath}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-1">
|
<div className="flex flex-row gap-1">
|
||||||
<UpdateVolume
|
<UpdateVolume
|
||||||
mountId={mount.mountId}
|
mountId={mount.mountId}
|
||||||
type={mount.type}
|
type={mount.type}
|
||||||
refetch={refetch}
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
@@ -19,10 +19,9 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Pencil } from "lucide-react";
|
import { PenBoxIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -78,7 +77,7 @@ export const UpdateVolume = ({
|
|||||||
serviceType,
|
serviceType,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const _utils = api.useUtils();
|
||||||
const { data } = api.mounts.one.useQuery(
|
const { data } = api.mounts.one.useQuery(
|
||||||
{
|
{
|
||||||
mountId,
|
mountId,
|
||||||
@@ -119,7 +118,7 @@ export const UpdateVolume = ({
|
|||||||
} else if (typeForm === "file") {
|
} else if (typeForm === "file") {
|
||||||
form.reset({
|
form.reset({
|
||||||
content: data.content || "",
|
content: data.content || "",
|
||||||
mountPath: "/",
|
mountPath: serviceType === "compose" ? "/" : data.mountPath,
|
||||||
filePath: data.filePath || "",
|
filePath: data.filePath || "",
|
||||||
type: "file",
|
type: "file",
|
||||||
});
|
});
|
||||||
@@ -140,7 +139,7 @@ export const UpdateVolume = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the Bind mount");
|
toast.error("Error updating the Bind mount");
|
||||||
});
|
});
|
||||||
} else if (data.type === "volume") {
|
} else if (data.type === "volume") {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -154,7 +153,7 @@ export const UpdateVolume = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the Volume mount");
|
toast.error("Error updating the Volume mount");
|
||||||
});
|
});
|
||||||
} else if (data.type === "file") {
|
} else if (data.type === "file") {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -169,7 +168,7 @@ export const UpdateVolume = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the File mount");
|
toast.error("Error updating the File mount");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
refetch();
|
refetch();
|
||||||
@@ -178,11 +177,16 @@ export const UpdateVolume = ({
|
|||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
<Button
|
||||||
<Pencil className="size-4 text-muted-foreground" />
|
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>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-3xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Update</DialogTitle>
|
<DialogTitle>Update</DialogTitle>
|
||||||
<DialogDescription>Update the mount</DialogDescription>
|
<DialogDescription>Update the mount</DialogDescription>
|
||||||
@@ -247,9 +251,12 @@ export const UpdateVolume = ({
|
|||||||
<FormLabel>Content</FormLabel>
|
<FormLabel>Content</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<CodeEditor
|
||||||
placeholder="Any content"
|
language="properties"
|
||||||
className="h-64"
|
placeholder={`NODE_ENV=production
|
||||||
|
PORT=3000
|
||||||
|
`}
|
||||||
|
className="h-96 font-mono"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
@@ -25,6 +26,7 @@ enum BuildType {
|
|||||||
paketo_buildpacks = "paketo_buildpacks",
|
paketo_buildpacks = "paketo_buildpacks",
|
||||||
nixpacks = "nixpacks",
|
nixpacks = "nixpacks",
|
||||||
static = "static",
|
static = "static",
|
||||||
|
railpack = "railpack",
|
||||||
}
|
}
|
||||||
|
|
||||||
const mySchema = z.discriminatedUnion("buildType", [
|
const mySchema = z.discriminatedUnion("buildType", [
|
||||||
@@ -41,6 +43,7 @@ const mySchema = z.discriminatedUnion("buildType", [
|
|||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
buildType: z.literal("heroku_buildpacks"),
|
buildType: z.literal("heroku_buildpacks"),
|
||||||
|
herokuVersion: z.string().nullable().default(""),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
buildType: z.literal("paketo_buildpacks"),
|
buildType: z.literal("paketo_buildpacks"),
|
||||||
@@ -52,6 +55,9 @@ const mySchema = z.discriminatedUnion("buildType", [
|
|||||||
z.object({
|
z.object({
|
||||||
buildType: z.literal("static"),
|
buildType: z.literal("static"),
|
||||||
}),
|
}),
|
||||||
|
z.object({
|
||||||
|
buildType: z.literal("railpack"),
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
type AddTemplate = z.infer<typeof mySchema>;
|
type AddTemplate = z.infer<typeof mySchema>;
|
||||||
@@ -90,6 +96,13 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
dockerBuildStage: data.dockerBuildStage || "",
|
dockerBuildStage: data.dockerBuildStage || "",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
} else if (data.buildType === "heroku_buildpacks") {
|
||||||
|
form.reset({
|
||||||
|
buildType: data.buildType,
|
||||||
|
...(data.buildType && {
|
||||||
|
herokuVersion: data.herokuVersion || "",
|
||||||
|
}),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
form.reset({
|
form.reset({
|
||||||
buildType: data.buildType,
|
buildType: data.buildType,
|
||||||
@@ -110,13 +123,15 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
data.buildType === "dockerfile" ? data.dockerContextPath : null,
|
data.buildType === "dockerfile" ? data.dockerContextPath : null,
|
||||||
dockerBuildStage:
|
dockerBuildStage:
|
||||||
data.buildType === "dockerfile" ? data.dockerBuildStage : null,
|
data.buildType === "dockerfile" ? data.dockerBuildStage : null,
|
||||||
|
herokuVersion:
|
||||||
|
data.buildType === "heroku_buildpacks" ? data.herokuVersion : null,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Build type saved");
|
toast.success("Build type saved");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to save the build type");
|
toast.error("Error saving the build type");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -163,6 +178,15 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
Dockerfile
|
Dockerfile
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
</FormItem>
|
</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">
|
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<RadioGroupItem value="nixpacks" />
|
<RadioGroupItem value="nixpacks" />
|
||||||
@@ -200,6 +224,28 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{buildType === "heroku_buildpacks" && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="herokuVersion"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Heroku Version (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder={"Heroku Version (Default: 24)"}
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{buildType === "dockerfile" && (
|
{buildType === "dockerfile" && (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
|
|||||||
@@ -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 { useRouter } from "next/router";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
applicationId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DeleteApplication = ({ applicationId }: Props) => {
|
|
||||||
const { mutateAsync, isLoading } = api.application.delete.useMutation();
|
|
||||||
const { push } = useRouter();
|
|
||||||
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
|
|
||||||
application
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
await mutateAsync({
|
|
||||||
applicationId,
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
push(`/dashboard/project/${data?.projectId}`);
|
|
||||||
|
|
||||||
toast.success("Application delete succesfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to delete Application");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -20,6 +20,12 @@ interface Props {
|
|||||||
|
|
||||||
export const CancelQueues = ({ applicationId }: Props) => {
|
export const CancelQueues = ({ applicationId }: Props) => {
|
||||||
const { mutateAsync, isLoading } = api.application.cleanQueues.useMutation();
|
const { mutateAsync, isLoading } = api.application.cleanQueues.useMutation();
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
|
if (isCloud) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { RefreshCcw } from "lucide-react";
|
import { RefreshCcw } from "lucide-react";
|
||||||
import React from "react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -47,7 +46,7 @@ export const RefreshToken = ({ applicationId }: Props) => {
|
|||||||
toast.success("Refresh updated");
|
toast.success("Refresh updated");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the refresh token");
|
toast.error("Error updating the refresh token");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -5,18 +7,45 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { TerminalLine } from "../../docker/logs/terminal-line";
|
||||||
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
logPath: string | null;
|
logPath: string | null;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
serverId?: string;
|
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 [data, setData] = useState("");
|
||||||
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
const [showExtraLogs, setShowExtraLogs] = useState(false);
|
||||||
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
||||||
|
const [autoScroll, setAutoScroll] = useState(true);
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
if (autoScroll && scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (!scrollRef.current) return;
|
||||||
|
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
||||||
|
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
|
||||||
|
setAutoScroll(isAtBottom);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !logPath) return;
|
if (!open || !logPath) return;
|
||||||
@@ -48,13 +77,36 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
|||||||
};
|
};
|
||||||
}, [logPath, open]);
|
}, [logPath, open]);
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
useEffect(() => {
|
||||||
endOfLogsRef.current?.scrollIntoView({ behavior: "smooth" });
|
const logs = parseLogs(data);
|
||||||
};
|
let filteredLogsResult = logs;
|
||||||
|
if (serverId) {
|
||||||
|
let hideSubsequentLogs = false;
|
||||||
|
filteredLogsResult = logs.filter((log) => {
|
||||||
|
if (
|
||||||
|
log.message.includes(
|
||||||
|
"===================================EXTRA LOGS============================================",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
hideSubsequentLogs = true;
|
||||||
|
return showExtraLogs;
|
||||||
|
}
|
||||||
|
return showExtraLogs ? true : !hideSubsequentLogs;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredLogs(filteredLogsResult);
|
||||||
|
}, [data, showExtraLogs]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, [data]);
|
|
||||||
|
if (autoScroll && scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [filteredLogs, autoScroll]);
|
||||||
|
|
||||||
|
const optionalErrors = parseLogs(errorMessage || "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -75,18 +127,57 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
|||||||
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Deployment</DialogTitle>
|
<DialogTitle>Deployment</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription className="flex items-center gap-2">
|
||||||
See all the details of this deployment
|
<span>
|
||||||
|
See all the details of this deployment |{" "}
|
||||||
|
<Badge variant="blank" className="text-xs">
|
||||||
|
{filteredLogs.length} lines
|
||||||
|
</Badge>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{serverId && (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="show-extra-logs"
|
||||||
|
checked={showExtraLogs}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setShowExtraLogs(checked as boolean)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="show-extra-logs"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
Show Extra Logs
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="text-wrap rounded-lg border p-4 text-sm sm:max-w-[59rem]">
|
<div
|
||||||
<code>
|
ref={scrollRef}
|
||||||
<pre className="whitespace-pre-wrap break-words">
|
onScroll={handleScroll}
|
||||||
{data || "Loading..."}
|
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
||||||
</pre>
|
>
|
||||||
<div ref={endOfLogsRef} />
|
{" "}
|
||||||
</code>
|
{filteredLogs.length > 0 ? (
|
||||||
|
filteredLogs.map((log: LogLine, index: number) => (
|
||||||
|
<TerminalLine key={index} log={log} noTimestamp />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{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>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { type RouterOutputs, api } from "@/utils/api";
|
||||||
import { RocketIcon } from "lucide-react";
|
import { RocketIcon } from "lucide-react";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { CancelQueues } from "./cancel-queues";
|
import { CancelQueues } from "./cancel-queues";
|
||||||
@@ -18,8 +18,11 @@ import { ShowDeployment } from "./show-deployment";
|
|||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDeployments = ({ applicationId }: Props) => {
|
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 } = api.application.one.useQuery({ applicationId });
|
||||||
const { data: deployments } = api.deployment.all.useQuery(
|
const { data: deployments } = api.deployment.all.useQuery(
|
||||||
{ applicationId },
|
{ applicationId },
|
||||||
@@ -28,6 +31,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
|||||||
refetchInterval: 1000,
|
refetchInterval: 1000,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const [url, setUrl] = React.useState("");
|
const [url, setUrl] = React.useState("");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUrl(document.location.origin);
|
setUrl(document.location.origin);
|
||||||
@@ -69,15 +73,14 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{deployments?.map((deployment) => (
|
{deployments?.map((deployment, index) => (
|
||||||
<div
|
<div
|
||||||
key={deployment.deploymentId}
|
key={deployment.deploymentId}
|
||||||
className="flex items-center justify-between rounded-lg border p-4 gap-2"
|
className="flex items-center justify-between rounded-lg border p-4 gap-2"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
||||||
{deployment.status}
|
{index + 1}. {deployment.status}
|
||||||
|
|
||||||
<StatusTooltip
|
<StatusTooltip
|
||||||
status={deployment?.status}
|
status={deployment?.status}
|
||||||
className="size-2.5"
|
className="size-2.5"
|
||||||
@@ -99,7 +102,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveLog(deployment.logPath);
|
setActiveLog(deployment);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
@@ -111,9 +114,10 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
<ShowDeployment
|
<ShowDeployment
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
open={activeLog !== null}
|
open={Boolean(activeLog && activeLog.logPath !== null)}
|
||||||
onClose={() => setActiveLog(null)}
|
onClose={() => setActiveLog(null)}
|
||||||
logPath={activeLog}
|
logPath={activeLog?.logPath || ""}
|
||||||
|
errorMessage={activeLog?.errorMessage || ""}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -104,9 +104,7 @@ export const AddDomain = ({
|
|||||||
|
|
||||||
const dictionary = {
|
const dictionary = {
|
||||||
success: domainId ? "Domain Updated" : "Domain Created",
|
success: domainId ? "Domain Updated" : "Domain Created",
|
||||||
error: domainId
|
error: domainId ? "Error updating the domain" : "Error creating the domain",
|
||||||
? "Error to update the domain"
|
|
||||||
: "Error to create the domain",
|
|
||||||
submit: domainId ? "Update" : "Create",
|
submit: domainId ? "Update" : "Create",
|
||||||
dialogDescription: domainId
|
dialogDescription: domainId
|
||||||
? "In this section you can edit a domain"
|
? "In this section you can edit a domain"
|
||||||
@@ -264,21 +262,21 @@ export const AddDomain = ({
|
|||||||
name="certificateType"
|
name="certificateType"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="col-span-2">
|
<FormItem className="col-span-2">
|
||||||
<FormLabel>Certificate</FormLabel>
|
<FormLabel>Certificate Provider</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={field.value || ""}
|
defaultValue={field.value || ""}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a certificate" />
|
<SelectValue placeholder="Select a certificate provider" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="none">None</SelectItem>
|
<SelectItem value="none">None</SelectItem>
|
||||||
<SelectItem value={"letsencrypt"}>
|
<SelectItem value={"letsencrypt"}>
|
||||||
Letsencrypt (Default)
|
Let's Encrypt
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -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 succesfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to delete Domain");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -6,19 +7,18 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { api } from "@/utils/api";
|
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 Link from "next/link";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { AddDomain } from "./add-domain";
|
import { AddDomain } from "./add-domain";
|
||||||
import { DeleteDomain } from "./delete-domain";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDomains = ({ applicationId }: Props) => {
|
export const ShowDomains = ({ applicationId }: Props) => {
|
||||||
const { data } = api.domain.byApplicationId.useQuery(
|
const { data, refetch } = api.domain.byApplicationId.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
},
|
},
|
||||||
@@ -26,6 +26,10 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
enabled: !!applicationId,
|
enabled: !!applicationId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { mutateAsync: deleteDomain, isLoading: isRemoving } =
|
||||||
|
api.domain.delete.useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
@@ -69,35 +73,66 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.domainId}
|
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
|
<Link
|
||||||
|
className="md:basis-1/2 flex gap-2 items-center hover:underline transition-all w-full"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
|
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>
|
</Link>
|
||||||
|
|
||||||
<Input disabled value={item.host} />
|
<div className="flex gap-8">
|
||||||
<Button variant="outline" disabled>
|
<div className="flex gap-8 opacity-50 items-center h-10 text-center text-sm font-medium">
|
||||||
{item.path}
|
<span>{item.path}</span>
|
||||||
</Button>
|
<span>{item.port}</span>
|
||||||
<Button variant="outline" disabled>
|
<span>{item.https ? "HTTPS" : "HTTP"}</span>
|
||||||
{item.port}
|
</div>
|
||||||
</Button>
|
|
||||||
<Button variant="outline" disabled>
|
<div className="flex gap-2">
|
||||||
{item.https ? "HTTPS" : "HTTP"}
|
<AddDomain
|
||||||
</Button>
|
applicationId={applicationId}
|
||||||
<div className="flex flex-row gap-1">
|
domainId={item.domainId}
|
||||||
<AddDomain
|
>
|
||||||
applicationId={applicationId}
|
<Button
|
||||||
domainId={item.domainId}
|
variant="ghost"
|
||||||
>
|
size="icon"
|
||||||
<Button variant="ghost">
|
className="group hover:bg-blue-500/10 "
|
||||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
>
|
||||||
</Button>
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</AddDomain>
|
</Button>
|
||||||
<DeleteDomain domainId={item.domainId} />
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+48
-20
@@ -18,10 +18,11 @@ import { Toggle } from "@/components/ui/toggle";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
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 { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import type { ServiceType } from "../advanced/show-resources";
|
||||||
|
|
||||||
const addEnvironmentSchema = z.object({
|
const addEnvironmentSchema = z.object({
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
@@ -30,21 +31,39 @@ const addEnvironmentSchema = z.object({
|
|||||||
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
|
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mariadbId: string;
|
id: string;
|
||||||
|
type: Exclude<ServiceType | "compose", "application">;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowMariadbEnvironment = ({ mariadbId }: 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 [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||||
const { mutateAsync, isLoading } = api.mariadb.saveEnvironment.useMutation();
|
|
||||||
|
|
||||||
const { data, refetch } = api.mariadb.one.useQuery(
|
const mutationMap = {
|
||||||
{
|
postgres: () => api.postgres.update.useMutation(),
|
||||||
mariadbId,
|
redis: () => api.redis.update.useMutation(),
|
||||||
},
|
mysql: () => api.mysql.update.useMutation(),
|
||||||
{
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
enabled: !!mariadbId,
|
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>({
|
const form = useForm<EnvironmentSchema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
environment: "",
|
environment: "",
|
||||||
@@ -62,22 +81,26 @@ export const ShowMariadbEnvironment = ({ mariadbId }: Props) => {
|
|||||||
|
|
||||||
const onSubmit = async (data: EnvironmentSchema) => {
|
const onSubmit = async (data: EnvironmentSchema) => {
|
||||||
mutateAsync({
|
mutateAsync({
|
||||||
|
mongoId: id || "",
|
||||||
|
postgresId: id || "",
|
||||||
|
redisId: id || "",
|
||||||
|
mysqlId: id || "",
|
||||||
|
mariadbId: id || "",
|
||||||
|
composeId: id || "",
|
||||||
env: data.environment,
|
env: data.environment,
|
||||||
mariadbId,
|
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Environments Added");
|
toast.success("Environments Added");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to add environment");
|
toast.error("Error adding environment");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
{" "}
|
|
||||||
<CardHeader className="flex flex-row w-full items-center justify-between">
|
<CardHeader className="flex flex-row w-full items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||||
@@ -109,19 +132,24 @@ export const ShowMariadbEnvironment = ({ mariadbId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="environment"
|
name="environment"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="w-full">
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl className="">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
WebkitTextSecurity: isEnvVisible ? "disc" : null,
|
||||||
|
} as CSSProperties
|
||||||
|
}
|
||||||
language="properties"
|
language="properties"
|
||||||
disabled={isEnvVisible}
|
disabled={isEnvVisible}
|
||||||
|
className="font-mono"
|
||||||
|
wrapperClassName="compose-file-editor"
|
||||||
placeholder={`NODE_ENV=production
|
placeholder={`NODE_ENV=production
|
||||||
PORT=3000
|
PORT=3000
|
||||||
`}
|
`}
|
||||||
className="h-96 font-mono"
|
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
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 { Form } from "@/components/ui/form";
|
||||||
import { Secrets } from "@/components/ui/secrets";
|
import { Secrets } from "@/components/ui/secrets";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -11,6 +11,7 @@ import { z } from "zod";
|
|||||||
const addEnvironmentSchema = z.object({
|
const addEnvironmentSchema = z.object({
|
||||||
env: z.string(),
|
env: z.string(),
|
||||||
buildArgs: z.string(),
|
buildArgs: z.string(),
|
||||||
|
buildSecrets: z.record(z.string(), z.string()),
|
||||||
});
|
});
|
||||||
|
|
||||||
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
|
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
|
||||||
@@ -36,6 +37,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
env: data?.env || "",
|
env: data?.env || "",
|
||||||
buildArgs: data?.buildArgs || "",
|
buildArgs: data?.buildArgs || "",
|
||||||
|
buildSecrets: data?.buildSecrets || {},
|
||||||
},
|
},
|
||||||
resolver: zodResolver(addEnvironmentSchema),
|
resolver: zodResolver(addEnvironmentSchema),
|
||||||
});
|
});
|
||||||
@@ -44,6 +46,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
mutateAsync({
|
mutateAsync({
|
||||||
env: data.env,
|
env: data.env,
|
||||||
buildArgs: data.buildArgs,
|
buildArgs: data.buildArgs,
|
||||||
|
buildSecrets: data.buildSecrets,
|
||||||
applicationId,
|
applicationId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
@@ -51,17 +54,17 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to add environment");
|
toast.error("Error adding environment");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Card className="bg-background px-6 pb-6">
|
||||||
<form
|
<Form {...form}>
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
<form
|
||||||
className="flex w-full flex-col gap-5 "
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
>
|
className="flex w-full flex-col gap-4"
|
||||||
<Card className="bg-background">
|
>
|
||||||
<Secrets
|
<Secrets
|
||||||
name="env"
|
name="env"
|
||||||
title="Environment Settings"
|
title="Environment Settings"
|
||||||
@@ -69,35 +72,71 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
placeholder={["NODE_ENV=production", "PORT=3000"].join("\n")}
|
placeholder={["NODE_ENV=production", "PORT=3000"].join("\n")}
|
||||||
/>
|
/>
|
||||||
{data?.buildType === "dockerfile" && (
|
{data?.buildType === "dockerfile" && (
|
||||||
<Secrets
|
<>
|
||||||
name="buildArgs"
|
<Secrets
|
||||||
title="Build-time Variables"
|
name="buildArgs"
|
||||||
description={
|
title="Build-time Variables"
|
||||||
<span>
|
description={
|
||||||
Available only at build-time. See documentation
|
<span>
|
||||||
<a
|
Available only at build-time. See documentation
|
||||||
className="text-primary"
|
<a
|
||||||
href="https://docs.docker.com/build/guide/build-args/"
|
className="text-primary"
|
||||||
target="_blank"
|
href="https://docs.docker.com/build/guide/build-args/"
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
>
|
rel="noopener noreferrer"
|
||||||
here
|
>
|
||||||
</a>
|
here
|
||||||
.
|
</a>
|
||||||
</span>
|
.
|
||||||
}
|
</span>
|
||||||
placeholder="NPM_TOKEN=xyz"
|
}
|
||||||
/>
|
placeholder="NPM_TOKEN=xyz"
|
||||||
|
/>
|
||||||
|
<Secrets
|
||||||
|
name="buildSecrets"
|
||||||
|
title="Build Secrets"
|
||||||
|
description={
|
||||||
|
<span>
|
||||||
|
Secrets available only during build-time and not in the
|
||||||
|
final image. See documentation
|
||||||
|
<a
|
||||||
|
className="text-primary"
|
||||||
|
href="https://docs.docker.com/build/building/secrets/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
placeholder="API_TOKEN=xyz"
|
||||||
|
transformValue={(value) => {
|
||||||
|
// Convert the string format to object
|
||||||
|
const lines = value.split("\n").filter((line) => line.trim());
|
||||||
|
return Object.fromEntries(
|
||||||
|
lines.map((line) => {
|
||||||
|
const [key, ...valueParts] = line.split("=");
|
||||||
|
return [key.trim(), valueParts.join("=").trim()];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
formatValue={(value) => {
|
||||||
|
// Convert the object back to string format
|
||||||
|
return Object.entries(value as Record<string, string>)
|
||||||
|
.map(([key, val]) => `${key}=${val}`)
|
||||||
|
.join("\n");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<CardContent>
|
<div className="flex flex-row justify-end">
|
||||||
<div className="flex flex-row justify-end">
|
<Button isLoading={isLoading} className="w-fit" type="submit">
|
||||||
<Button isLoading={isLoading} className="w-fit" type="submit">
|
Save
|
||||||
Save
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</CardContent>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,69 +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 { toast } from "sonner";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
applicationId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DeployApplication = ({ applicationId }: Props) => {
|
|
||||||
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 succesfully");
|
|
||||||
await refetch();
|
|
||||||
})
|
|
||||||
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to deploy Application");
|
|
||||||
});
|
|
||||||
|
|
||||||
await refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+8
-6
@@ -84,7 +84,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
data: repositories,
|
data: repositories,
|
||||||
isLoading: isLoadingRepositories,
|
isLoading: isLoadingRepositories,
|
||||||
error,
|
error,
|
||||||
isError,
|
|
||||||
} = api.bitbucket.getBitbucketRepositories.useQuery(
|
} = api.bitbucket.getBitbucketRepositories.useQuery(
|
||||||
{
|
{
|
||||||
bitbucketId,
|
bitbucketId,
|
||||||
@@ -137,7 +136,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to save the Bitbucket provider");
|
toast.error("Error saving the Bitbucket provider");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -202,7 +201,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full justify-between !bg-input",
|
"w-full justify-between !bg-input",
|
||||||
!field.value && "text-muted-foreground",
|
!field.value && "text-muted-foreground",
|
||||||
@@ -236,7 +234,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{repositories?.map((repo) => (
|
{repositories?.map((repo) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={repo.url}
|
value={repo.name}
|
||||||
key={repo.url}
|
key={repo.url}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
form.setValue("repository", {
|
form.setValue("repository", {
|
||||||
@@ -246,7 +244,12 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
form.setValue("branch", "");
|
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
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
@@ -281,7 +284,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
" w-full justify-between !bg-input",
|
" w-full justify-between !bg-input",
|
||||||
!field.value && "text-muted-foreground",
|
!field.value && "text-muted-foreground",
|
||||||
|
|||||||
+19
-3
@@ -21,6 +21,7 @@ const DockerProviderSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
username: z.string().optional(),
|
username: z.string().optional(),
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
|
registryURL: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
@@ -33,12 +34,12 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
|||||||
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
||||||
|
|
||||||
const { mutateAsync } = api.application.saveDockerProvider.useMutation();
|
const { mutateAsync } = api.application.saveDockerProvider.useMutation();
|
||||||
|
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
dockerImage: "",
|
dockerImage: "",
|
||||||
password: "",
|
password: "",
|
||||||
username: "",
|
username: "",
|
||||||
|
registryURL: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
});
|
});
|
||||||
@@ -49,6 +50,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
|||||||
dockerImage: data.dockerImage || "",
|
dockerImage: data.dockerImage || "",
|
||||||
password: data.password || "",
|
password: data.password || "",
|
||||||
username: data.username || "",
|
username: data.username || "",
|
||||||
|
registryURL: data.registryUrl || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
@@ -59,13 +61,14 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
|||||||
password: values.password || null,
|
password: values.password || null,
|
||||||
applicationId,
|
applicationId,
|
||||||
username: values.username || null,
|
username: values.username || null,
|
||||||
|
registryUrl: values.registryURL || null,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Docker Provider Saved");
|
toast.success("Docker Provider Saved");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to save the Docker provider");
|
toast.error("Error saving the Docker provider");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76,7 +79,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
|||||||
className="flex flex-col gap-4"
|
className="flex flex-col gap-4"
|
||||||
>
|
>
|
||||||
<div className="grid md:grid-cols-2 gap-4 ">
|
<div className="grid md:grid-cols-2 gap-4 ">
|
||||||
<div className="md:col-span-2 space-y-4">
|
<div className="space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="dockerImage"
|
name="dockerImage"
|
||||||
@@ -91,6 +94,19 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="registryURL"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Registry URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Registry URL" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const SaveDragNDrop = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to save the deployment");
|
toast.error("Error saving the deployment");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to save the Git provider");
|
toast.error("Error saving the Git provider");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+8
-5
@@ -131,7 +131,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to save the github provider");
|
toast.error("Error saving the github provider");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -193,7 +193,6 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full justify-between !bg-input",
|
"w-full justify-between !bg-input",
|
||||||
!field.value && "text-muted-foreground",
|
!field.value && "text-muted-foreground",
|
||||||
@@ -227,7 +226,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{repositories?.map((repo) => (
|
{repositories?.map((repo) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={repo.url}
|
value={repo.name}
|
||||||
key={repo.url}
|
key={repo.url}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
form.setValue("repository", {
|
form.setValue("repository", {
|
||||||
@@ -237,7 +236,12 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
form.setValue("branch", "");
|
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
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
@@ -272,7 +276,6 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
" w-full justify-between !bg-input",
|
" w-full justify-between !bg-input",
|
||||||
!field.value && "text-muted-foreground",
|
!field.value && "text-muted-foreground",
|
||||||
|
|||||||
+8
-5
@@ -144,7 +144,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to save the gitlab provider");
|
toast.error("Error saving the gitlab provider");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -209,7 +209,6 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full justify-between !bg-input",
|
"w-full justify-between !bg-input",
|
||||||
!field.value && "text-muted-foreground",
|
!field.value && "text-muted-foreground",
|
||||||
@@ -249,7 +248,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
{repositories?.map((repo) => {
|
{repositories?.map((repo) => {
|
||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={repo.url}
|
value={repo.name}
|
||||||
key={repo.url}
|
key={repo.url}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
form.setValue("repository", {
|
form.setValue("repository", {
|
||||||
@@ -261,7 +260,12 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
form.setValue("branch", "");
|
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
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
@@ -297,7 +301,6 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
" w-full justify-between !bg-input",
|
" w-full justify-between !bg-input",
|
||||||
!field.value && "text-muted-foreground",
|
!field.value && "text-muted-foreground",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { GitBranch, LockIcon, UploadCloud } from "lucide-react";
|
import { GitBranch, UploadCloud } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { SaveBitbucketProvider } from "./save-bitbucket-provider";
|
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 to reload the service");
|
|
||||||
});
|
|
||||||
await refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,23 +1,34 @@
|
|||||||
import { ShowBuildChooseForm } from "@/components/dashboard/application/build/show";
|
import { ShowBuildChooseForm } from "@/components/dashboard/application/build/show";
|
||||||
import { ShowProviderForm } from "@/components/dashboard/application/general/generic/show";
|
import { ShowProviderForm } from "@/components/dashboard/application/general/generic/show";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Toggle } from "@/components/ui/toggle";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { CheckCircle2, Terminal } from "lucide-react";
|
import {
|
||||||
import React from "react";
|
Ban,
|
||||||
|
CheckCircle2,
|
||||||
|
Hammer,
|
||||||
|
HelpCircle,
|
||||||
|
RefreshCcw,
|
||||||
|
Terminal,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
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 {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||||
|
const router = useRouter();
|
||||||
const { data, refetch } = api.application.one.useQuery(
|
const { data, refetch } = api.application.one.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
@@ -25,6 +36,17 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
{ enabled: !!applicationId },
|
{ enabled: !!applicationId },
|
||||||
);
|
);
|
||||||
const { mutateAsync: update } = api.application.update.useMutation();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -33,39 +55,188 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<DeployApplication applicationId={applicationId} />
|
<TooltipProvider delayDuration={0}>
|
||||||
<ResetApplication
|
<DialogAction
|
||||||
applicationId={applicationId}
|
title="Deploy Application"
|
||||||
appName={data?.appName || ""}
|
description="Are you sure you want to deploy this application?"
|
||||||
/>
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
<Toggle
|
await deploy({
|
||||||
aria-label="Toggle italic"
|
applicationId: applicationId,
|
||||||
pressed={data?.autoDeploy || false}
|
|
||||||
onPressedChange={async (enabled) => {
|
|
||||||
await update({
|
|
||||||
applicationId,
|
|
||||||
autoDeploy: enabled,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Auto Deploy Updated");
|
|
||||||
await refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error("Error to update Auto Deploy");
|
toast.success("Application deployed successfully");
|
||||||
});
|
refetch();
|
||||||
}}
|
router.push(
|
||||||
className="flex flex-row gap-2 items-center"
|
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`,
|
||||||
>
|
);
|
||||||
Autodeploy
|
})
|
||||||
{data?.autoDeploy && <CheckCircle2 className="size-4" />}
|
.catch(() => {
|
||||||
</Toggle>
|
toast.error("Error deploying application");
|
||||||
<RedbuildApplication applicationId={applicationId} />
|
});
|
||||||
{data?.applicationStatus === "idle" ? (
|
}}
|
||||||
<StartApplication applicationId={applicationId} />
|
>
|
||||||
) : (
|
<Button
|
||||||
<StopApplication applicationId={applicationId} />
|
variant="default"
|
||||||
)}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Deploy
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Downloads the source code and performs a complete build
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</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"}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Rebuild
|
||||||
|
<Hammer className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Only rebuilds the application without downloading new
|
||||||
|
code
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
|
||||||
|
{data?.applicationStatus === "idle" ? (
|
||||||
|
<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}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
<CheckCircle2 className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Start the application (requires a previous successful
|
||||||
|
build)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
) : (
|
||||||
|
<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}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
<Ban className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Stop the currently running application</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
@@ -75,6 +246,27 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
Open Terminal
|
Open Terminal
|
||||||
</Button>
|
</Button>
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
|
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||||
|
<span className="text-sm font-medium">Autodeploy</span>
|
||||||
|
<Switch
|
||||||
|
aria-label="Toggle italic"
|
||||||
|
checked={data?.autoDeploy || false}
|
||||||
|
onCheckedChange={async (enabled) => {
|
||||||
|
await update({
|
||||||
|
applicationId,
|
||||||
|
autoDeploy: enabled,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Auto Deploy Updated");
|
||||||
|
await refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error updating Auto Deploy");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="flex flex-row gap-2 items-center"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<ShowProviderForm applicationId={applicationId} />
|
<ShowProviderForm applicationId={applicationId} />
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -15,6 +16,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
@@ -29,28 +31,67 @@ export const DockerLogs = dynamic(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const badgeStateColor = (state: string) => {
|
||||||
|
switch (state) {
|
||||||
|
case "running":
|
||||||
|
return "green";
|
||||||
|
case "exited":
|
||||||
|
case "shutdown":
|
||||||
|
return "red";
|
||||||
|
case "accepted":
|
||||||
|
case "created":
|
||||||
|
return "blue";
|
||||||
|
default:
|
||||||
|
return "default";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
appName: string;
|
appName: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
||||||
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
|
||||||
{
|
|
||||||
appName,
|
|
||||||
serverId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!appName,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const [containerId, setContainerId] = useState<string | undefined>();
|
const [containerId, setContainerId] = useState<string | undefined>();
|
||||||
|
const [option, setOption] = useState<"swarm" | "native">("native");
|
||||||
|
|
||||||
|
const { data: services, isLoading: servicesLoading } =
|
||||||
|
api.docker.getServiceContainersByAppName.useQuery(
|
||||||
|
{
|
||||||
|
appName,
|
||||||
|
serverId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!appName && option === "swarm",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: containers, isLoading: containersLoading } =
|
||||||
|
api.docker.getContainersByAppNameMatch.useQuery(
|
||||||
|
{
|
||||||
|
appName,
|
||||||
|
serverId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!appName && option === "native",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data && data?.length > 0) {
|
if (option === "native") {
|
||||||
setContainerId(data[0]?.containerId);
|
if (containers && containers?.length > 0) {
|
||||||
|
setContainerId(containers[0]?.containerId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (services && services?.length > 0) {
|
||||||
|
setContainerId(services[0]?.containerId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [option, services, containers]);
|
||||||
|
|
||||||
|
const isLoading = option === "native" ? containersLoading : servicesLoading;
|
||||||
|
const containersLenght =
|
||||||
|
option === "native" ? containers?.length : services?.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
@@ -62,7 +103,21 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
<Label>Select a container to view logs</Label>
|
<div className="flex flex-row justify-between items-center gap-2">
|
||||||
|
<Label>Select a container to view logs</Label>
|
||||||
|
<div className="flex flex-row gap-2 items-center">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{option === "native" ? "Native" : "Swarm"}
|
||||||
|
</span>
|
||||||
|
<Switch
|
||||||
|
checked={option === "native"}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setOption(checked ? "native" : "swarm");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Select onValueChange={setContainerId} value={containerId}>
|
<Select onValueChange={setContainerId} value={containerId}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@@ -76,22 +131,45 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
{data?.map((container) => (
|
{option === "native" ? (
|
||||||
<SelectItem
|
<div>
|
||||||
key={container.containerId}
|
{containers?.map((container) => (
|
||||||
value={container.containerId}
|
<SelectItem
|
||||||
>
|
key={container.containerId}
|
||||||
{container.name} ({container.containerId}) {container.state}
|
value={container.containerId}
|
||||||
</SelectItem>
|
>
|
||||||
))}
|
{container.name} ({container.containerId}){" "}
|
||||||
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
<Badge variant={badgeStateColor(container.state)}>
|
||||||
|
{container.state}
|
||||||
|
</Badge>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{services?.map((container) => (
|
||||||
|
<SelectItem
|
||||||
|
key={container.containerId}
|
||||||
|
value={container.containerId}
|
||||||
|
>
|
||||||
|
{container.name} ({container.containerId}@{container.node}
|
||||||
|
)
|
||||||
|
<Badge variant={badgeStateColor(container.state)}>
|
||||||
|
{container.state}
|
||||||
|
</Badge>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SelectLabel>Containers ({containersLenght})</SelectLabel>
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<DockerLogs
|
<DockerLogs
|
||||||
serverId={serverId || ""}
|
serverId={serverId || ""}
|
||||||
id="terminal"
|
|
||||||
containerId={containerId || "select-a-container"}
|
containerId={containerId || "select-a-container"}
|
||||||
|
runType={option}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
+302
@@ -0,0 +1,302 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input, NumberInput } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
import { domain } from "@/server/db/validations/domain";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Dices } from "lucide-react";
|
||||||
|
import type z from "zod";
|
||||||
|
|
||||||
|
type Domain = z.infer<typeof domain>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
previewDeploymentId: string;
|
||||||
|
domainId?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddPreviewDomain = ({
|
||||||
|
previewDeploymentId,
|
||||||
|
domainId = "",
|
||||||
|
children,
|
||||||
|
}: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { data, refetch } = api.domain.one.useQuery(
|
||||||
|
{
|
||||||
|
domainId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!domainId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: previewDeployment } = api.previewDeployment.one.useQuery(
|
||||||
|
{
|
||||||
|
previewDeploymentId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!previewDeploymentId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync, isError, error, isLoading } = domainId
|
||||||
|
? api.domain.update.useMutation()
|
||||||
|
: api.domain.create.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
|
||||||
|
api.domain.generateDomain.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<Domain>({
|
||||||
|
resolver: zodResolver(domain),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
...data,
|
||||||
|
/* Convert null to undefined */
|
||||||
|
path: data?.path || undefined,
|
||||||
|
port: data?.port || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!domainId) {
|
||||||
|
form.reset({});
|
||||||
|
}
|
||||||
|
}, [form, form.reset, data, isLoading]);
|
||||||
|
|
||||||
|
const dictionary = {
|
||||||
|
success: domainId ? "Domain Updated" : "Domain Created",
|
||||||
|
error: domainId ? "Error updating the domain" : "Error creating the domain",
|
||||||
|
submit: domainId ? "Update" : "Create",
|
||||||
|
dialogDescription: domainId
|
||||||
|
? "In this section you can edit a domain"
|
||||||
|
: "In this section you can add domains",
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data: Domain) => {
|
||||||
|
await mutateAsync({
|
||||||
|
domainId,
|
||||||
|
previewDeploymentId,
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success(dictionary.success);
|
||||||
|
await utils.previewDeployment.all.invalidate({
|
||||||
|
applicationId: previewDeployment?.applicationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (domainId) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error(dictionary.error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger className="" asChild>
|
||||||
|
{children}
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Domain</DialogTitle>
|
||||||
|
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-8 "
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="host"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Host</FormLabel>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="api.dokploy.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingGenerate}
|
||||||
|
onClick={() => {
|
||||||
|
generateDomain({
|
||||||
|
appName: previewDeployment?.appName || "",
|
||||||
|
serverId:
|
||||||
|
previewDeployment?.application
|
||||||
|
?.serverId || "",
|
||||||
|
})
|
||||||
|
.then((domain) => {
|
||||||
|
field.onChange(domain);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(err.message);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dices className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>Generate traefik.me domain</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="path"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Path</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder={"/"} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="port"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Container Port</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<NumberInput placeholder={"3000"} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="https"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>HTTPS</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Automatically provision SSL Certificate.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{form.getValues().https && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="certificateType"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="col-span-2">
|
||||||
|
<FormLabel>Certificate Provider</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value || ""}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a certificate provider" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">None</SelectItem>
|
||||||
|
<SelectItem value={"letsencrypt"}>
|
||||||
|
Let's Encrypt
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button isLoading={isLoading} form="hook-form" type="submit">
|
||||||
|
{dictionary.submit}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
import type { RouterOutputs } from "@/utils/api";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ShowDeployment } from "../deployments/show-deployment";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
deployments: RouterOutputs["deployment"]["all"];
|
||||||
|
serverId?: string;
|
||||||
|
trigger?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowPreviewBuilds = ({
|
||||||
|
deployments,
|
||||||
|
serverId,
|
||||||
|
trigger,
|
||||||
|
}: Props) => {
|
||||||
|
const [activeLog, setActiveLog] = useState<
|
||||||
|
RouterOutputs["deployment"]["all"][number] | null
|
||||||
|
>(null);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
{trigger ? (
|
||||||
|
trigger
|
||||||
|
) : (
|
||||||
|
<Button className="sm:w-auto w-full" size="sm" variant="outline">
|
||||||
|
View Builds
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Preview Builds</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
See all the preview builds for this application on this Pull Request
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{deployments?.map((deployment) => (
|
||||||
|
<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}
|
||||||
|
|
||||||
|
<StatusTooltip
|
||||||
|
status={deployment?.status}
|
||||||
|
className="size-2.5"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{deployment.title}
|
||||||
|
</span>
|
||||||
|
{deployment.description && (
|
||||||
|
<span className="break-all text-sm text-muted-foreground">
|
||||||
|
{deployment.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-end gap-2">
|
||||||
|
<div className="text-sm capitalize text-muted-foreground">
|
||||||
|
<DateTooltip date={deployment.createdAt} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setActiveLog(deployment);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
<ShowDeployment
|
||||||
|
serverId={serverId || ""}
|
||||||
|
open={Boolean(activeLog && activeLog.logPath !== null)}
|
||||||
|
onClose={() => setActiveLog(null)}
|
||||||
|
logPath={activeLog?.logPath || ""}
|
||||||
|
errorMessage={activeLog?.errorMessage || ""}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
+238
@@ -0,0 +1,238 @@
|
|||||||
|
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import {
|
||||||
|
ExternalLink,
|
||||||
|
FileText,
|
||||||
|
GitPullRequest,
|
||||||
|
Layers,
|
||||||
|
PenSquare,
|
||||||
|
RocketIcon,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
|
||||||
|
import { AddPreviewDomain } from "./add-preview-domain";
|
||||||
|
import { ShowPreviewBuilds } from "./show-preview-builds";
|
||||||
|
import { ShowPreviewSettings } from "./show-preview-settings";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
applicationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
||||||
|
const { data } = api.application.one.useQuery({ applicationId });
|
||||||
|
|
||||||
|
const { mutateAsync: deletePreviewDeployment, isLoading } =
|
||||||
|
api.previewDeployment.delete.useMutation();
|
||||||
|
|
||||||
|
const { data: previewDeployments, refetch: refetchPreviewDeployments } =
|
||||||
|
api.previewDeployment.all.useQuery(
|
||||||
|
{ applicationId },
|
||||||
|
{
|
||||||
|
enabled: !!applicationId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeletePreviewDeployment = async (previewDeploymentId: string) => {
|
||||||
|
deletePreviewDeployment({
|
||||||
|
previewDeploymentId: previewDeploymentId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
refetchPreviewDeployments();
|
||||||
|
toast.success("Preview deployment deleted");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<CardTitle className="text-xl">Preview Deployments</CardTitle>
|
||||||
|
<CardDescription>See all the preview deployments</CardDescription>
|
||||||
|
</div>
|
||||||
|
{data?.isPreviewDeploymentsActive && (
|
||||||
|
<ShowPreviewSettings applicationId={applicationId} />
|
||||||
|
)}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
{data?.isPreviewDeploymentsActive ? (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-2 text-sm">
|
||||||
|
<span>
|
||||||
|
Preview deployments are a way to test your application before it
|
||||||
|
is deployed to production. It will create a new deployment for
|
||||||
|
each pull request you create.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{!previewDeployments?.length ? (
|
||||||
|
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||||
|
<RocketIcon className="size-8 text-muted-foreground" />
|
||||||
|
<span className="text-base text-muted-foreground">
|
||||||
|
No preview deployments found
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{previewDeployments?.map((deployment) => {
|
||||||
|
const deploymentUrl = `${deployment.domain?.https ? "https" : "http"}://${deployment.domain?.host}${deployment.domain?.path || "/"}`;
|
||||||
|
const status = deployment.previewStatus;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={deployment.previewDeploymentId}
|
||||||
|
className="group relative overflow-hidden border rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`absolute left-0 top-0 w-1 h-full ${
|
||||||
|
status === "done"
|
||||||
|
? "bg-green-500"
|
||||||
|
: status === "running"
|
||||||
|
? "bg-yellow-500"
|
||||||
|
: "bg-red-500"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<GitPullRequest className="size-5 text-muted-foreground mt-1 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-sm">
|
||||||
|
{deployment.pullRequestTitle}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground mt-1">
|
||||||
|
{deployment.branch}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline" className="gap-2">
|
||||||
|
<StatusTooltip
|
||||||
|
status={deployment.previewStatus}
|
||||||
|
className="size-2"
|
||||||
|
/>
|
||||||
|
<DateTooltip date={deployment.createdAt} />
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pl-8 space-y-3">
|
||||||
|
<div className="relative flex-grow">
|
||||||
|
<Input
|
||||||
|
value={deploymentUrl}
|
||||||
|
readOnly
|
||||||
|
className="pr-8 text-sm text-blue-500 hover:text-blue-600 cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
window.open(deploymentUrl, "_blank")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ExternalLink className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 opacity-80 group-hover:opacity-100 transition-opacity">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2"
|
||||||
|
onClick={() =>
|
||||||
|
window.open(deployment.pullRequestURL, "_blank")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<GithubIcon className="size-4" />
|
||||||
|
Pull Request
|
||||||
|
</Button>
|
||||||
|
<ShowModalLogs
|
||||||
|
appName={deployment.appName}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<FileText className="size-4" />
|
||||||
|
Logs
|
||||||
|
</Button>
|
||||||
|
</ShowModalLogs>
|
||||||
|
|
||||||
|
<ShowPreviewBuilds
|
||||||
|
deployments={deployment.deployments || []}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<Layers className="size-4" />
|
||||||
|
Builds
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AddPreviewDomain
|
||||||
|
previewDeploymentId={`${deployment.previewDeploymentId}`}
|
||||||
|
domainId={deployment.domain?.domainId}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<PenSquare className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</AddPreviewDomain>
|
||||||
|
<DialogAction
|
||||||
|
title="Delete Preview"
|
||||||
|
description="Are you sure you want to delete this preview?"
|
||||||
|
onClick={() =>
|
||||||
|
handleDeletePreviewDeployment(
|
||||||
|
deployment.previewDeploymentId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
isLoading={isLoading}
|
||||||
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||||
|
<RocketIcon className="size-8 text-muted-foreground" />
|
||||||
|
<span className="text-base text-muted-foreground">
|
||||||
|
Preview deployments are disabled for this application, please
|
||||||
|
enable it
|
||||||
|
</span>
|
||||||
|
<ShowPreviewSettings applicationId={applicationId} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
+345
@@ -0,0 +1,345 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input, NumberInput } from "@/components/ui/input";
|
||||||
|
import { Secrets } from "@/components/ui/secrets";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Settings2 } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
env: z.string(),
|
||||||
|
buildArgs: z.string(),
|
||||||
|
wildcardDomain: z.string(),
|
||||||
|
port: z.number(),
|
||||||
|
previewLimit: z.number(),
|
||||||
|
previewHttps: z.boolean(),
|
||||||
|
previewPath: z.string(),
|
||||||
|
previewCertificateType: z.enum(["letsencrypt", "none"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Schema = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
applicationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isEnabled, setIsEnabled] = useState(false);
|
||||||
|
const { mutateAsync: updateApplication, isLoading } =
|
||||||
|
api.application.update.useMutation();
|
||||||
|
|
||||||
|
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
||||||
|
|
||||||
|
const form = useForm<Schema>({
|
||||||
|
defaultValues: {
|
||||||
|
env: "",
|
||||||
|
wildcardDomain: "*.traefik.me",
|
||||||
|
port: 3000,
|
||||||
|
previewLimit: 3,
|
||||||
|
previewHttps: false,
|
||||||
|
previewPath: "/",
|
||||||
|
previewCertificateType: "none",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewHttps = form.watch("previewHttps");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsEnabled(data?.isPreviewDeploymentsActive || false);
|
||||||
|
}, [data?.isPreviewDeploymentsActive]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
env: data.previewEnv || "",
|
||||||
|
buildArgs: data.previewBuildArgs || "",
|
||||||
|
wildcardDomain: data.previewWildcard || "*.traefik.me",
|
||||||
|
port: data.previewPort || 3000,
|
||||||
|
previewLimit: data.previewLimit || 3,
|
||||||
|
previewHttps: data.previewHttps || false,
|
||||||
|
previewPath: data.previewPath || "/",
|
||||||
|
previewCertificateType: data.previewCertificateType || "none",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const onSubmit = async (formData: Schema) => {
|
||||||
|
updateApplication({
|
||||||
|
previewEnv: formData.env,
|
||||||
|
previewBuildArgs: formData.buildArgs,
|
||||||
|
previewWildcard: formData.wildcardDomain,
|
||||||
|
previewPort: formData.port,
|
||||||
|
applicationId,
|
||||||
|
previewLimit: formData.previewLimit,
|
||||||
|
previewHttps: formData.previewHttps,
|
||||||
|
previewPath: formData.previewPath,
|
||||||
|
previewCertificateType: formData.previewCertificateType,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Preview Deployments settings updated");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline">
|
||||||
|
<Settings2 className="size-4" />
|
||||||
|
Configure
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl w-full">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Preview Deployment Settings</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Adjust the settings for preview deployments of this application,
|
||||||
|
including environment variables, build options, and deployment
|
||||||
|
rules.
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
<div className="grid gap-4 lg:grid-cols-2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="wildcardDomain"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Wildcard Domain</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="*.traefik.me" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="previewPath"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Preview Path</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="/" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="port"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Port</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<NumberInput placeholder="3000" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="previewLimit"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Preview Limit</FormLabel>
|
||||||
|
{/* <FormDescription>
|
||||||
|
Set the limit of preview deployments that can be
|
||||||
|
created for this app.
|
||||||
|
</FormDescription> */}
|
||||||
|
<FormControl>
|
||||||
|
<NumberInput placeholder="3000" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="previewHttps"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>HTTPS</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Automatically provision SSL Certificate.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{previewHttps && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="previewCertificateType"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Certificate Provider</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value || ""}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a certificate provider" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">None</SelectItem>
|
||||||
|
<SelectItem value={"letsencrypt"}>
|
||||||
|
Let's Encrypt
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-4 lg:grid-cols-2">
|
||||||
|
<div className="flex flex-row items-center justify-between rounded-lg border p-4 col-span-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel className="text-base">
|
||||||
|
Enable preview deployments
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Enable or disable preview deployments for this
|
||||||
|
application.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={isEnabled}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
updateApplication({
|
||||||
|
isPreviewDeploymentsActive: checked,
|
||||||
|
applicationId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
refetch();
|
||||||
|
toast.success("Preview deployments enabled");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="env"
|
||||||
|
render={() => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Secrets
|
||||||
|
name="env"
|
||||||
|
title="Environment Settings"
|
||||||
|
description="You can add environment variables to your resource."
|
||||||
|
placeholder={[
|
||||||
|
"NODE_ENV=production",
|
||||||
|
"PORT=3000",
|
||||||
|
].join("\n")}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{data?.buildType === "dockerfile" && (
|
||||||
|
<Secrets
|
||||||
|
name="buildArgs"
|
||||||
|
title="Build-time Variables"
|
||||||
|
description={
|
||||||
|
<span>
|
||||||
|
Available only at build-time. See documentation
|
||||||
|
<a
|
||||||
|
className="text-primary"
|
||||||
|
href="https://docs.docker.com/build/guide/build-args/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
placeholder="NPM_TOKEN=xyz"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
form="hook-form-delete-application"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
{/* */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 to rebuild 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 succesfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to start 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 succesfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to stop the Application");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user