Compare commits
792 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d9043e590 | |||
| 59c3e8817c | |||
| 3132b3035a | |||
| 7ebf5ca16a | |||
| d96f132dc0 | |||
| b26ff08a3c | |||
| 44678cf5f1 | |||
| 5084ee761e | |||
| 91f50ae949 | |||
| 15f04edcf1 | |||
| 01945675ed | |||
| 185c16c3e2 | |||
| d25814b84b | |||
| a09a3fd012 | |||
| 871fd83148 | |||
| dd8bc493e7 | |||
| 44d3baf51a | |||
| c85c0558b9 | |||
| 7f11699fac | |||
| 525e48ae59 | |||
| a6369a7dde | |||
| d5ea99603f | |||
| 083432cbfe | |||
| dbbce4160d | |||
| 885aac69c5 | |||
| b3c301fc2a | |||
| 01deb4d285 | |||
| aeddaa4566 | |||
| eb314ef507 | |||
| 620c49cf76 | |||
| 6d73dfdb40 | |||
| 232cdb8cb1 | |||
| fd53174099 | |||
| 9bf240d83b | |||
| 421567ff14 | |||
| ce05008fce | |||
| a250cb9fe2 | |||
| 012f8bc14e | |||
| 11dce549bd | |||
| 5b567ac941 | |||
| 5b103c78e5 | |||
| bc96f9c5e5 | |||
| d97d82b225 | |||
| e9b6002f63 | |||
| 704999a05f | |||
| ba99e3b9b7 | |||
| 9adeaed1b9 | |||
| 10bd2e930f | |||
| 07396c9824 | |||
| bf34b13b7f | |||
| 0bab0ed748 | |||
| 8754a208b1 | |||
| 19100c7999 | |||
| d98ec77abf | |||
| 34c2decd91 | |||
| 09fb4ea89f | |||
| d6bb32aead | |||
| 3a21191fba | |||
| ad4e0ba9aa | |||
| baf598099f | |||
| 7d017d83d6 | |||
| 7911780a16 | |||
| 1dc6b40a68 | |||
| dd9e6e63ad | |||
| 30633e7820 | |||
| acfc67ed0a | |||
| a99a8ef382 | |||
| 7aec713e6c | |||
| 60c4dd3875 | |||
| 7d8ba8d42c | |||
| 7ff7842441 | |||
| bcf497b460 | |||
| bf51a60986 | |||
| 41809d1ca8 | |||
| acb1445270 | |||
| 86530df37e | |||
| b64b513b14 | |||
| 285feb4f8b | |||
| c6f4395578 | |||
| 4981854c7f | |||
| 65f6ff35e0 | |||
| d892cc5763 | |||
| d122ece8e9 | |||
| a363e508b6 | |||
| e481606d0e | |||
| a1e2c9fd08 | |||
| f5931abdeb | |||
| 4c9e05b08f | |||
| 9c34727e88 | |||
| 939cd94ebb | |||
| 4a33415b06 | |||
| 082e659c7b | |||
| a8059c49e9 | |||
| f7b14b2ee8 | |||
| 581a294af1 | |||
| 40e8ba38f0 | |||
| 65f4ec0f43 | |||
| 8748ba1226 | |||
| 2ad8ee18a0 | |||
| 8b9dc5a6bf | |||
| a1a6570ee3 | |||
| 6c68f7da2e | |||
| ccd5f6c2e5 | |||
| 73b20ae809 | |||
| a4dd6cc928 | |||
| 6f37bde55d | |||
| 8f1f3cea18 | |||
| f715e21306 | |||
| d9b478cf1f | |||
| 36a768eb60 | |||
| 0ebe6e545d | |||
| 5fb295e044 | |||
| be7e11a4f6 | |||
| d13bf9ac74 | |||
| e909b6e643 | |||
| 9555a93ddc | |||
| 1d9ad35019 | |||
| 78aee78d9c | |||
| 4b96d5879c | |||
| 20396a8c7d | |||
| 8510b12841 | |||
| 345f9541fe | |||
| c1c864ced7 | |||
| 7a440a32d1 | |||
| ef1431f89b | |||
| 57cf0ec074 | |||
| d795507ddd | |||
| c3aafb2979 | |||
| 93d4472932 | |||
| 69934be88c | |||
| 8638bfb30b | |||
| 5b3d6a3957 | |||
| 94dd0644d0 | |||
| f089329e12 | |||
| 8554d374c9 | |||
| 424ec1c90d | |||
| ce2f1b4170 | |||
| ce1146a9ef | |||
| f065821587 | |||
| 18c518f385 | |||
| 6ba1953acb | |||
| 324ee4641f | |||
| 9ee4490498 | |||
| 0fa32c9572 | |||
| b3e5563e15 | |||
| 55038dee51 | |||
| b54eaf2964 | |||
| 98e97a0f9b | |||
| f15b4a4f4b | |||
| bd7641f502 | |||
| 2d04ec2308 | |||
| ba0ab68f50 | |||
| 825fe21bd9 | |||
| 072ca73259 | |||
| b333a136e8 | |||
| e34cf7fd77 | |||
| 1c7099b3f0 | |||
| d74ee441ac | |||
| 7bad90009e | |||
| 424bc588f6 | |||
| 852e1e1687 | |||
| 649c06b641 | |||
| 6b4df0bd65 | |||
| e67324b05c | |||
| d688f5e080 | |||
| c3f9d688f1 | |||
| 7affc6e987 | |||
| 9f26608681 | |||
| d34b102e52 | |||
| 077f95049e | |||
| b570f2f77d | |||
| b4e7e394c3 | |||
| e1f87161a8 | |||
| 57bf3709f3 | |||
| 9d258d33cf | |||
| 62e322c451 | |||
| 9a04ee2d1f | |||
| 5852fac71a | |||
| f315a378dc | |||
| dcee792aaa | |||
| d0df897f93 | |||
| 915e3ecc94 | |||
| 76dadd1f8b | |||
| 73fdcae916 | |||
| 941162a05f | |||
| 22b8ec6144 | |||
| a0c7f3f896 | |||
| 692be297b3 | |||
| db3eabcd2f | |||
| fee78bb488 | |||
| dda6f43b8a | |||
| b5fad74ea0 | |||
| ef42106a16 | |||
| bba1922120 | |||
| f386c326e2 | |||
| 3b26f6f5ea | |||
| 52701e1173 | |||
| 3c7d24916c | |||
| 4fac0cb535 | |||
| 00f6ef7603 | |||
| 556b9fe20c | |||
| 3dcd0975f7 | |||
| db7e88e302 | |||
| 9c5fb2823c | |||
| d12b2ae2db | |||
| 449d68122b | |||
| 0227ae1d96 | |||
| 265e58e5cb | |||
| 9054f30aef | |||
| 36887abf88 | |||
| 4ca5fcf472 | |||
| c4b01dea22 | |||
| 6d4cc4a6b8 | |||
| 4229e9921c | |||
| 92b6a7e335 | |||
| cb8731b915 | |||
| 0c80b7af1d | |||
| 4ce9c46215 | |||
| bbf402368f | |||
| 37d1dc7c6d | |||
| a677dc3981 | |||
| 77163cc1b2 | |||
| 5d41a84fec | |||
| 890de53b0a | |||
| a1f2b5b696 | |||
| 6eda037544 | |||
| eb5b8b42dc | |||
| 4a5022d14d | |||
| 54c6f9c4f8 | |||
| dee60e9958 | |||
| bbefa38355 | |||
| 6681f2e5c8 | |||
| 1728756dc4 | |||
| 1f0860e45d | |||
| 9eb91a3ae9 | |||
| ad50ea5aee | |||
| 73045fd7fc | |||
| 11aeccc822 | |||
| 310a8c1c63 | |||
| 23153e5b86 | |||
| 130d8a1ba0 | |||
| 8d9ecf3352 | |||
| 6080cfa351 | |||
| 4e04b2075f | |||
| 9f415826fd | |||
| 54d92b8bf7 | |||
| f1e8b91f61 | |||
| a1bd1a0fa1 | |||
| b142cd5039 | |||
| b548856c29 | |||
| a0df52000a | |||
| e98a1a9767 | |||
| ad2eaff60e | |||
| 3df7b74f65 | |||
| 67c1b2cb71 | |||
| 6c0e84a31d | |||
| c49a440211 | |||
| caedf6a8e7 | |||
| 203330d1b8 | |||
| c8d66384c7 | |||
| 74447d2690 | |||
| b66ddedc86 | |||
| 8df97de8c6 | |||
| cd5cae33ce | |||
| 608ce53e7d | |||
| d2ae6c2353 | |||
| 7eda1136ea | |||
| a756fa9e9b | |||
| afb5e5ac5d | |||
| efa1acddd4 | |||
| e00db115ad | |||
| 366f247910 | |||
| 2a6368af60 | |||
| 5420630453 | |||
| 4e39eb89fd | |||
| a783944700 | |||
| 8a987db177 | |||
| 834a7d0f55 | |||
| 051bcb7819 | |||
| 126587ba82 | |||
| 860ebcbe6a | |||
| 25f395ed63 | |||
| 2da361a1f2 | |||
| 4e363dc77a | |||
| 23e20b9b83 | |||
| e70a6ffbb9 | |||
| cab236123f | |||
| cab7e0d8a3 | |||
| 2f425f8119 | |||
| 017e46fa0f | |||
| 9efcd9060e | |||
| abdd5d3e0e | |||
| cf40346e1a | |||
| b6d80fb443 | |||
| f6e4f1aefc | |||
| dbf66b8e89 | |||
| 53ad3902ac | |||
| cae2bfbdc2 | |||
| 58d6142460 | |||
| 2ca4838ac7 | |||
| 3787f90283 | |||
| 9064375e25 | |||
| 033036bd1a | |||
| 5d74d80829 | |||
| 88231e3d35 | |||
| 1aa683aeab | |||
| c2326bc5cc | |||
| 55db3ae517 | |||
| 4b0dbf0183 | |||
| 2725e001a5 | |||
| 02a0f65e4b | |||
| 9fd964022e | |||
| ec7dabc1c7 | |||
| 95eeb9ce93 | |||
| d137cdf881 | |||
| a926a3e8a8 | |||
| e8b3516d34 | |||
| 54e5e0cb7e | |||
| baa4620523 | |||
| fcd1532a4d | |||
| 66b768b176 | |||
| eeae8c92d0 | |||
| d35bfbb0fd | |||
| 4516b0c57c | |||
| 49243822af | |||
| 16521d5434 | |||
| 1afa2e87ec | |||
| 18ec929501 | |||
| 7d6636bb54 | |||
| 3c7e6b59f0 | |||
| daa8a60da2 | |||
| f231d51d0b | |||
| 308f315ed5 | |||
| a572374ad7 | |||
| 1cf315634c | |||
| b0d2bdbad9 | |||
| 255fb0cac0 | |||
| c3be0018fe | |||
| 37e2269387 | |||
| 5dbe2ce2e4 | |||
| 1008ec4fa1 | |||
| d36d1cf1da | |||
| 21d7438bbe | |||
| caf1c37171 | |||
| 0a748ac78a | |||
| 76c4002a04 | |||
| 201a07f717 | |||
| 5b2eb51511 | |||
| 36ab5800a3 | |||
| a79486275e | |||
| 6dc70a8f3b | |||
| 8e990e4e0a | |||
| f11becfcc8 | |||
| 8d04374764 | |||
| 87ae95aa4f | |||
| 0fa1ec44b1 | |||
| b4e4f26361 | |||
| 2afaf1f36d | |||
| f236213356 | |||
| efd0be5e2c | |||
| 6612f48d0a | |||
| f1679f1614 | |||
| 8b7dca00af | |||
| 59fa26b0fb | |||
| 7a92222050 | |||
| be2775e12e | |||
| 6c3f8b9b84 | |||
| f02157857c | |||
| 470b0d6be7 | |||
| 2b1b304c6e | |||
| 5460a64951 | |||
| 62faf616c5 | |||
| 3f2f4c7c6b | |||
| 5e49a33e8f | |||
| 5fb7d53018 | |||
| 424a3c2b53 | |||
| 6e629b984b | |||
| c73609211a | |||
| e5477351f8 | |||
| d89f8d99a3 | |||
| da472dff19 | |||
| 2dc501dcbd | |||
| 052b705c3c | |||
| 24c8fca971 | |||
| 86edabee4d | |||
| d6f162a8ca | |||
| 9e05a4eab7 | |||
| 32d9490856 | |||
| 91d9f66eb8 | |||
| 86986d8f34 | |||
| 03ef9f109f | |||
| 67a8228886 | |||
| 8db6da2de9 | |||
| 544b8180b2 | |||
| 2515b032d0 | |||
| 6086b0e797 | |||
| 2760e25c0f | |||
| 76aa0b4a70 | |||
| 0e23687c7f | |||
| 028b820d48 | |||
| 2c81458954 | |||
| ebe1883f8e | |||
| 030e468829 | |||
| 68724bcb4f | |||
| 6186bb54e4 | |||
| a4e822dec2 | |||
| 5744cb7318 | |||
| 2f6a66dbd7 | |||
| 91d3980e3b | |||
| 3ddf72a24d | |||
| a6f4b2896a | |||
| c79ddbf948 | |||
| de99b8ecce | |||
| 8b0bcde7ec | |||
| d862f1f5b4 | |||
| 1c4f6315a6 | |||
| 44eaac6685 | |||
| a89576965d | |||
| 774f14327c | |||
| 6bd9391160 | |||
| a82fb0c2cb | |||
| 110e683318 | |||
| 781ee77280 | |||
| adc9894fde | |||
| c7bf5f2abc | |||
| 601e868afc | |||
| 25b1259c4c | |||
| 1a8a111c79 | |||
| 497b3016c0 | |||
| fe9bd52b04 | |||
| 0705c77333 | |||
| b66e77a2d8 | |||
| 4b4c8d8052 | |||
| 4ee56782ba | |||
| 104997d77c | |||
| 8e07b3c96d | |||
| 4e618540f8 | |||
| 49941a34b9 | |||
| 771b797a23 | |||
| d09915bf6e | |||
| 264c94ff34 | |||
| a90df99331 | |||
| 78f0d61627 | |||
| 8c106b3435 | |||
| 42555c7231 | |||
| ab035a2afe | |||
| 3a30eed3cd | |||
| 4cb390374b | |||
| 50179dd7eb | |||
| 2956c3360c | |||
| c634bdbd34 | |||
| 1892c0cd80 | |||
| 63b395982c | |||
| d50c8539b2 | |||
| 90c8348c9c | |||
| 1426cbec81 | |||
| 7047915995 | |||
| 49b514054f | |||
| bf27374dcc | |||
| 3de1c6e499 | |||
| d77285f2c4 | |||
| 96eeb70076 | |||
| 6a39e442ac | |||
| 91e030be44 | |||
| 405e20e18e | |||
| 138f770630 | |||
| eadc4fda30 | |||
| 35c5f19eac | |||
| 6d8ae180b3 | |||
| 0fea30969f | |||
| 3ff8f5cb33 | |||
| b6162a3bef | |||
| 09ca67f408 | |||
| cadb1ad674 | |||
| dec7bc3ca3 | |||
| d87460a3cd | |||
| f076711ad3 | |||
| 6149a5ac12 | |||
| 44c61f7bd7 | |||
| 4ea47da269 | |||
| 35f2c0ec7d | |||
| 3316dd1f42 | |||
| 07527fe2b1 | |||
| 03207f62ba | |||
| bcc78d81a6 | |||
| 0d38e443d1 | |||
| 50a069a7fa | |||
| 7455490074 | |||
| 64bb53abc3 | |||
| 18a680a85b | |||
| e26f71b603 | |||
| f98fe43843 | |||
| 26dad82cd3 | |||
| 73d1860995 | |||
| aca5c254d2 | |||
| 3521815646 | |||
| aecc16af5d | |||
| 5927f397a3 | |||
| 1e85c86e74 | |||
| 6640929b01 | |||
| 7a333ace11 | |||
| 32bce25ad5 | |||
| 5dc023d801 | |||
| e3f31e6560 | |||
| e582e147cb | |||
| 6525504923 | |||
| 6d6107161e | |||
| 3196864f0d | |||
| d7596beaf3 | |||
| 23de5b5a61 | |||
| d98b09f802 | |||
| 97c012b3df | |||
| 867b8e0253 | |||
| 80400db92a | |||
| 72ff84be47 | |||
| 13e62bc738 | |||
| 0e83658aa3 | |||
| 8e4506382d | |||
| 7a0b74d79b | |||
| 1026728ab7 | |||
| 909fe5dc15 | |||
| aed9801501 | |||
| 41f92c52e9 | |||
| d0dc104ede | |||
| ce42680888 | |||
| 4ebff09f73 | |||
| 8fd7daade6 | |||
| e6aef755e3 | |||
| c4b8d3b832 | |||
| c38457b48d | |||
| 60994f9ed1 | |||
| a6f078330f | |||
| cfd5c0f82b | |||
| 14c9260ab0 | |||
| 23cac99fe9 | |||
| 2237d2bbb7 | |||
| 62ca0487dc | |||
| 0e858dc333 | |||
| fa3e908afc | |||
| c1bb4de6a3 | |||
| 9b94cf18d0 | |||
| b51071155a | |||
| 1128edc23e | |||
| df9c7010e2 | |||
| 54c7757e38 | |||
| 3da3ccedcb | |||
| 26eb739b46 | |||
| 7ce5b53753 | |||
| 298d446e5f | |||
| 450dd70ea2 | |||
| 1d1a7af48e | |||
| 003bc457bf | |||
| bfafcea0b9 | |||
| 66da8dd4dc | |||
| 307a3ee015 | |||
| 95be147eb4 | |||
| 2bf711f1f7 | |||
| c3d2c7bcde | |||
| 38e32942cb | |||
| febd24b203 | |||
| d1afa3fdca | |||
| a82d1ea832 | |||
| 7d9e8da660 | |||
| ec990bd16a | |||
| fb12c0e499 | |||
| 3d1a4f8802 | |||
| c978e3b7ea | |||
| 0b201cee71 | |||
| 8b7c5a65d6 | |||
| 8a63f0368e | |||
| ce4bf7e10c | |||
| 479946173f | |||
| 176baa075f | |||
| bfbc41d5a7 | |||
| d2b303ffd6 | |||
| 00bbb4242d | |||
| 0a4b0688a8 | |||
| 9efe399399 | |||
| b03240ccb8 | |||
| 35eb17a922 | |||
| c8b997f732 | |||
| 80e83e0c05 | |||
| 9491b5aa39 | |||
| 243a254f3e | |||
| 2d1e0ec890 | |||
| 793ee38f79 | |||
| 5240068f2f | |||
| b8be174610 | |||
| b923925a6c | |||
| 61f5669d76 | |||
| cf707ba657 | |||
| 660260336c | |||
| 0447086882 | |||
| 29a96e5df1 | |||
| c95bb248fb | |||
| d3551826c1 | |||
| d2c21627de | |||
| 81e21effa4 | |||
| 2d03941745 | |||
| 2401c9cee7 | |||
| 4f0bbcc73b | |||
| 5b9700e099 | |||
| d7dda61775 | |||
| 3220721f84 | |||
| 0ed144fe81 | |||
| 13b9bed48b | |||
| c99c24b3bd | |||
| bd1ab000f3 | |||
| a1fd5bb996 | |||
| 9ef29343b3 | |||
| 8bdcdd7810 | |||
| a1217e52c8 | |||
| a8d37b917a | |||
| 06ce351d82 | |||
| f43a601e86 | |||
| 0dfadc5922 | |||
| c8cd67258a | |||
| 7499aa9201 | |||
| 0f4ea17f29 | |||
| b7631689b0 | |||
| afe670b49c | |||
| ee43dff35f | |||
| 1faf83afe4 | |||
| ce0b66db7d | |||
| 01d33c45bd | |||
| 63766dd10f | |||
| 8771158f10 | |||
| 46a589f794 | |||
| a007a8e40c | |||
| 6e42cf4ec5 | |||
| 257dc4e271 | |||
| 4136272382 | |||
| 4f9e43859c | |||
| b57ad9b8c1 | |||
| b8c297b178 | |||
| a389b863f9 | |||
| 40c82b3e48 | |||
| 2ca94f3159 | |||
| 33a97d0e50 | |||
| cef0b6d0d8 | |||
| 7a5e990ad4 | |||
| ca31dc8d78 | |||
| 5b7667fa4d | |||
| 6cdb448f62 | |||
| 053f81a53e | |||
| c842d02d6f | |||
| 4ddcd547ba | |||
| 7bb68ea6b5 | |||
| e13f427267 | |||
| c422e2d570 | |||
| b3f91c4868 | |||
| 19dd56c160 | |||
| c577d3d91f | |||
| 4f57bed03a | |||
| 29663a1229 | |||
| d9d4798f69 | |||
| 32d3c0b920 | |||
| 2224ccab7c | |||
| 8d3d3ba875 | |||
| 4ad2b2829b | |||
| 1ca46a064c | |||
| e42579521c | |||
| 96be06188b | |||
| 10172e0211 | |||
| 70c8a5a6be | |||
| af42f150f2 | |||
| ba16fdaf60 | |||
| c5480bfcc1 | |||
| 79448e9ff9 | |||
| e49398eb47 | |||
| fa842034ed | |||
| 672b472359 | |||
| 37ed87f9c1 | |||
| 25ba312636 | |||
| 340ea3fe9b | |||
| d264f8b05c | |||
| 54672d9fce | |||
| 5ac9a7f1ef | |||
| b906b0f7f2 | |||
| 758e1965f1 | |||
| 8ff437c4d2 | |||
| 4374124985 | |||
| 8b5afaa12c | |||
| a54c6d3c32 | |||
| 93af9379bd | |||
| 39deb41e2e | |||
| d7c0a947fb | |||
| 09b438850e | |||
| cbefd4195f | |||
| 849c8bf6ac | |||
| 00268b1da9 | |||
| 5f5e6084d7 | |||
| 852c4d1300 | |||
| 81fe6f884b | |||
| 9780e4184e | |||
| 1af1660312 | |||
| 1206f5dc88 | |||
| 793c4ac017 | |||
| 620e3af525 | |||
| c7b2e15d16 | |||
| 48f0c75c57 | |||
| 93d3b24300 | |||
| 21f830eb8c | |||
| c195cb00c0 | |||
| f7a53e1b15 | |||
| 759f3f29f0 | |||
| be35926fd1 | |||
| 45fd046b9b | |||
| 2b8d0f60e7 | |||
| 0e0199fc94 | |||
| 7a730c445b | |||
| 4d29592450 | |||
| 44be454a1e | |||
| cbf1b47332 | |||
| eb64bd296a | |||
| 72083f59cd | |||
| 8a20b603f5 | |||
| d45c433bc7 | |||
| 470417fcbe | |||
| 8e28d2a5aa | |||
| 344578006c | |||
| e19fd5cf17 | |||
| 943325baa3 | |||
| 702de2557e | |||
| 159f3419a5 | |||
| b1fb3bccd8 | |||
| 8927634636 | |||
| b9e584752b | |||
| 5857c05e01 | |||
| 81eb4bdebb | |||
| da18427125 | |||
| df0b4ace5e | |||
| 5971d3bf77 | |||
| cca3138f05 | |||
| 242c091add | |||
| 6f0788c9e4 | |||
| 15132a30da | |||
| 3245370280 | |||
| 740c0fe318 | |||
| 8d20ca2053 | |||
| cdd8e34cfc | |||
| a056bcfdfe | |||
| b5065a381f | |||
| 56324e3e8e | |||
| e64182d791 | |||
| 573eaee287 | |||
| 771bfd0244 | |||
| 2db96a5242 | |||
| 8459d231c2 | |||
| efd42b7293 | |||
| fe1c483b78 | |||
| bf381aff7f | |||
| 1a43c05d48 | |||
| 804a3f8adb | |||
| 1122137d12 | |||
| b88afbac4e | |||
| 8e468788a9 | |||
| 7f9e5303be | |||
| 08c48df862 | |||
| 1bc3875519 | |||
| c69cf4731a | |||
| 4ad5bd71f1 | |||
| 1ddc1cec20 | |||
| 934c701be2 | |||
| fadd4165df | |||
| 538454b11b | |||
| e4464afd56 | |||
| eb1f3d8b55 | |||
| e7208278fc | |||
| e87370354b | |||
| fc3bd3a0fe | |||
| 2270f5789a | |||
| 7ef20c273e | |||
| 39942dc5b0 | |||
| 37a6e60e90 | |||
| 1f8c55f536 | |||
| 36c4772b17 | |||
| 47d7536e24 | |||
| 9d9a407c3d | |||
| 7d731d7600 | |||
| dd9db22e9c | |||
| 6830c4fc67 | |||
| 2f3fba346f | |||
| 5bae308cae | |||
| ed71f9ac68 | |||
| 5e7bc78d35 | |||
| 41319bc817 | |||
| ceb908bee7 | |||
| 0e195679bf | |||
| 9c78b2df9a | |||
| 4844f6d927 | |||
| 64381e2a04 |
+1
-1
@@ -5,7 +5,7 @@
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020
|
||||
"ecmaVersion": 13
|
||||
},
|
||||
"rules": {
|
||||
"linebreak-style": [
|
||||
|
||||
@@ -2698,3 +2698,119 @@
|
||||
* oidc: add oidc logo as login indicator for apps
|
||||
* dyndns: update dns every 10 mins
|
||||
|
||||
[7.6.1]
|
||||
* Cleanup backup validation mount point
|
||||
* dashboard: remove nginx config of old domain when domain changed
|
||||
* Show disk consumption of docker volumes for /run and /tmp of apps separately
|
||||
* dns: add dnsimple automation
|
||||
* roles: admin role can access branding and networking
|
||||
* dns: add ovh backend
|
||||
|
||||
[7.6.2]
|
||||
* mail: fix issue with redis emitting warnings non-stop
|
||||
* mail: fix issue where doublle header was sent
|
||||
* ovh: fix nameserver matching
|
||||
* logviewer: preserve horizontal scroll position
|
||||
* redis: use default instead of redisuser
|
||||
* dockerproxy: allow child containers to access volumes
|
||||
* dashboard: Show system information
|
||||
* Fix linode object storage
|
||||
* postgres: enable cube, vector and earthdistance extensions
|
||||
* Add ability to register a Cloudron with a setupToken only
|
||||
* support: replace ticket section with help section
|
||||
* firewall: increase blocklist size to 262144
|
||||
|
||||
[7.6.3]
|
||||
* postgres: do not clear search_path for restore
|
||||
* route53: retry on rate limit errors
|
||||
* update: continue with app update if box update does not start
|
||||
|
||||
[7.6.4]
|
||||
* mail: update limit plugin
|
||||
* ldap: fix error messages to show proper error messages in the external LDAP connector
|
||||
* dashboard: fix various UI elements hidden for admin user
|
||||
* directoryserver: fix totp validation
|
||||
* email: improve loading of the mail usage to not block other views from loading
|
||||
* eventlog: add events for directory server and exernal directory configuration
|
||||
* externalldap: available regardless of subscription
|
||||
* externalldap: show syncer log history
|
||||
* externalldap: sync is now run periodically (every 4 hours)
|
||||
* profile: changing email now requires password
|
||||
|
||||
[7.7.0]
|
||||
* OIDC avatar support via picture claim
|
||||
* backupcleaner: fix bug where preserved backups were removed incorrectly
|
||||
* directoryserver: cloudflare warning
|
||||
* oidc/ldap: fix display name parsing to send anything after first name as the last name
|
||||
* mail: Update haraka to 3.0.3
|
||||
* mongodb: Update mongodb to 6.0
|
||||
* acme: use secp256r1 curve for max compatibility
|
||||
* add port range support
|
||||
* docker: disable userland proxy
|
||||
* oidc: always re-setup oidc client record
|
||||
* mail: update solr to 8.11.3
|
||||
* mail: spam acl should allow underscore and question mark
|
||||
* Fix streaming of logs with `logPaths`
|
||||
* profile: store user language setting in the database
|
||||
|
||||
[7.7.1]
|
||||
* postgresql: fix bug in loading of contrib extensions
|
||||
* dashboard: use native slider element for app memory and cpu
|
||||
|
||||
[7.7.2]
|
||||
* docker: use unix domain socket based logging instead of udp
|
||||
* dashboard: use native slider element for app memory and cpu
|
||||
* filemanager: fix empty folder content layout
|
||||
* dashboard: preserve app link paths
|
||||
* backups: deleted apps must also be displayed in contents
|
||||
* filemanager: make uploads cancellable
|
||||
* Fix crash on systemds with no swap
|
||||
|
||||
[8.0.0]
|
||||
* mongodb: optionally start mongodb based on AVX support
|
||||
* dashboard: font and color improvements
|
||||
* docker: prune volumes on infra change
|
||||
* oidc: initial login of admin and normal user now gets an OIDC session
|
||||
* branding: default background image for the dashboard
|
||||
* dashboard: list view for apps
|
||||
* import: fix crash when using mountpoint provider
|
||||
* dashboard: set '/' as keyboard shortcut
|
||||
* app: memory limit is redefined to be just RAM and unlimited swap
|
||||
* dashboard: rework filter UI
|
||||
* cpu: rework cpu shares into cpu quota
|
||||
* cifs: enable seal encryption by default
|
||||
* updatechecker: fix bug where release info was not refreshed
|
||||
* ovh: storage location domain has changed. add rbx region
|
||||
* domains: add deSEC integration
|
||||
* notfound: better message when navigating by IP address
|
||||
* IPv6 only server installation
|
||||
* Initial Ubuntu 24.04 (Noble Numbat) support
|
||||
* syslog: handle potential multiline syslog input
|
||||
* user directory: fixes to mandatory 2fa setting when cloudron connector is used
|
||||
* notification: do not send login notification for external users
|
||||
* dashboard: pending checklist indicator
|
||||
* cloudron-support: add --recreate-docker and --recreate-container
|
||||
* filemanager: add dark mode
|
||||
* proxyauth: now uses oidc instead of ldap auth
|
||||
* dashboard: add admin notes
|
||||
* Use systemd-resolved as the system resolver. unbound is now only for mail server and recursive DNS lookups
|
||||
|
||||
[8.0.1]
|
||||
* nfs: disable rpcbind service. we only support nfsv4 mounting
|
||||
* dashboard: only show postinstall if notes are not just empty
|
||||
* ami: disable route53
|
||||
* mailer: add html version of test mail
|
||||
* sshfs: server side copying
|
||||
* backups: rewrite tgz backups using tar-stream
|
||||
* backups: fix issue with s3 backend where files missing in remote was not detected correctly
|
||||
* provision: redirect to correct task (setup/restore/activation)
|
||||
|
||||
[8.0.2]
|
||||
* tgz: fix unhandled promise error handler
|
||||
* tgz: add underflow/overflow proxy stream to ensure size of a changing file
|
||||
* backups: give task a low oomScoreAdjust to not get killed
|
||||
* Fix issue with uploads via File Manager where temp files were not cleaned up
|
||||
* addons: fix crash when importing database of an app with no addons
|
||||
* sshfs: if remote copy fails, fallback to sshfs based copy
|
||||
* frontend: reduce DOM node creation on very fast logstreams and cap to 1k loglines
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs'),
|
||||
ldap = require('./src/ldap.js'),
|
||||
const constants = require('./src/constants.js'),
|
||||
fs = require('fs'),
|
||||
ldapServer = require('./src/ldapserver.js'),
|
||||
oidc = require('./src/oidc.js'),
|
||||
paths = require('./src/paths.js'),
|
||||
proxyAuth = require('./src/proxyauth.js'),
|
||||
@@ -14,7 +15,7 @@ const fs = require('fs'),
|
||||
let logFd;
|
||||
|
||||
async function setupLogging() {
|
||||
if (process.env.BOX_ENV === 'test') return;
|
||||
if (constants.TEST) return;
|
||||
|
||||
logFd = fs.openSync(paths.BOX_LOG_FILE, 'a');
|
||||
// we used to write using a stream before but it caches internally and there is no way to flush it when things crash
|
||||
@@ -36,7 +37,7 @@ async function startServers() {
|
||||
await setupLogging();
|
||||
await server.start(); // do this first since it also inits the database
|
||||
await proxyAuth.start();
|
||||
await ldap.start();
|
||||
await ldapServer.start();
|
||||
|
||||
const conf = await directoryServer.getConfig();
|
||||
if (conf.enabled) await directoryServer.start();
|
||||
@@ -61,7 +62,7 @@ async function main() {
|
||||
await proxyAuth.stop();
|
||||
await server.stop();
|
||||
await directoryServer.stop();
|
||||
await ldap.stop();
|
||||
await ldapServer.stop();
|
||||
await oidc.stop();
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
@@ -72,7 +73,7 @@ async function main() {
|
||||
await proxyAuth.stop();
|
||||
await server.stop();
|
||||
await directoryServer.stop();
|
||||
await ldap.stop();
|
||||
await ldapServer.stop();
|
||||
await oidc.stop();
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
+22
-25
@@ -3,9 +3,7 @@
|
||||
'use strict';
|
||||
|
||||
const argv = require('yargs').argv,
|
||||
autoprefixer = require('gulp-autoprefixer'),
|
||||
concat = require('gulp-concat'),
|
||||
cssnano = require('gulp-cssnano'),
|
||||
ejs = require('gulp-ejs'),
|
||||
execSync = require('child_process').execSync,
|
||||
fs = require('fs'),
|
||||
@@ -48,6 +46,11 @@ gulp.task('fontawesome', function () {
|
||||
.pipe(gulp.dest('dist/3rdparty/fontawesome/'));
|
||||
});
|
||||
|
||||
gulp.task('noto-sans', function () {
|
||||
return gulp.src('node_modules/@fontsource/noto-sans/**/*')
|
||||
.pipe(gulp.dest('dist/3rdparty/noto-sans/'));
|
||||
});
|
||||
|
||||
gulp.task('bootstrap', function () {
|
||||
return gulp.src('node_modules/bootstrap-sass/assets/javascripts/bootstrap.min.js')
|
||||
.pipe(gulp.dest('dist/3rdparty/js'));
|
||||
@@ -72,7 +75,7 @@ gulp.task('3rdparty-copy', function () {
|
||||
]).pipe(gulp.dest('dist/3rdparty/'));
|
||||
});
|
||||
|
||||
gulp.task('3rdparty', gulp.series(['3rdparty-copy', 'moment', 'bootstrap', 'fontawesome']));
|
||||
gulp.task('3rdparty', gulp.series(['3rdparty-copy', 'moment', 'bootstrap', 'fontawesome', 'noto-sans']));
|
||||
|
||||
// --------------
|
||||
// JavaScript
|
||||
@@ -110,6 +113,15 @@ gulp.task('js-setupaccount', function () {
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-activation', function () {
|
||||
return gulp.src(['src/js/activation.js', 'src/js/client.js', 'src/js/utils.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('activation.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-setup', function () {
|
||||
return gulp.src(['src/js/setup.js', 'src/js/client.js', 'src/js/utils.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
@@ -119,15 +131,6 @@ gulp.task('js-setup', function () {
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-setupdns', function () {
|
||||
return gulp.src(['src/js/setupdns.js', 'src/js/client.js', 'src/js/utils.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('setupdns.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-restore', function () {
|
||||
return gulp.src(['src/js/restore.js', 'src/js/client.js', 'src/js/utils.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
@@ -137,7 +140,7 @@ gulp.task('js-restore', function () {
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js', gulp.series([ 'js-index', 'js-passwordreset', 'js-setupaccount', 'js-setup', 'js-setupdns', 'js-restore' ]));
|
||||
gulp.task('js', gulp.series([ 'js-index', 'js-passwordreset', 'js-setupaccount', 'js-activation', 'js-setup', 'js-restore' ]));
|
||||
|
||||
// --------------
|
||||
// HTML
|
||||
@@ -147,15 +150,11 @@ gulp.task('html-views', function () {
|
||||
return gulp.src('src/views/**/*.html').pipe(gulp.dest('dist/views'));
|
||||
});
|
||||
|
||||
gulp.task('html-templates', function () {
|
||||
return gulp.src('src/templates/**/*').pipe(gulp.dest('dist/templates'));
|
||||
});
|
||||
|
||||
gulp.task('html-raw', function () {
|
||||
return gulp.src('src/*.html').pipe(ejs({ apiOrigin: apiOrigin, revision: revision }, {}, { ext: '.html' })).pipe(gulp.dest('dist'));
|
||||
});
|
||||
|
||||
gulp.task('html', gulp.series(['html-views', 'html-templates', 'html-raw']));
|
||||
gulp.task('html', gulp.series(['html-views', 'html-raw']));
|
||||
|
||||
// --------------
|
||||
// CSS
|
||||
@@ -163,11 +162,10 @@ gulp.task('html', gulp.series(['html-views', 'html-templates', 'html-raw']));
|
||||
|
||||
gulp.task('css', function () {
|
||||
return gulp.src('src/*.scss')
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(sass({ includePaths: ['node_modules/bootstrap-sass/assets/stylesheets/'] }).on('error', sass.logError))
|
||||
.pipe(autoprefixer())
|
||||
.pipe(cssnano())
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(sass({ includePaths: [
|
||||
'node_modules/bootstrap-sass/assets/stylesheets/',
|
||||
'node_modules/@fontsource/'
|
||||
]}).on('error', sass.logError))
|
||||
.pipe(gulp.dest('dist'));
|
||||
});
|
||||
|
||||
@@ -202,10 +200,9 @@ gulp.task('watch', function (done) {
|
||||
gulp.watch(['src/translation/*'], gulp.series(['translation']));
|
||||
gulp.watch(['src/**/*.html'], gulp.series(['html']));
|
||||
gulp.watch(['src/views/*.html'], gulp.series(['html-views']));
|
||||
gulp.watch(['src/templates/*.html'], gulp.series(['html-templates']));
|
||||
gulp.watch(['scripts/createTimezones.js', 'src/js/utils.js'], gulp.series(['timezones']));
|
||||
gulp.watch(['src/js/activation.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-activation']));
|
||||
gulp.watch(['src/js/setup.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-setup']));
|
||||
gulp.watch(['src/js/setupdns.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-setupdns']));
|
||||
gulp.watch(['src/js/restore.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-restore']));
|
||||
gulp.watch(['src/js/passwordreset.js', 'src/js/utils.js'], gulp.series(['js-passwordreset']));
|
||||
gulp.watch(['src/js/setupaccount.js', 'src/js/utils.js'], gulp.series(['js-setupaccount']));
|
||||
|
||||
Generated
+40
-3425
File diff suppressed because it is too large
Load Diff
@@ -13,19 +13,18 @@
|
||||
"author": "",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.4.0",
|
||||
"@fontsource/noto-sans": "^5.0.21",
|
||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"bootstrap-sass": "^3.4.3",
|
||||
"chart.js": "^4.3.0",
|
||||
"chart.js": "^4.4.2",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-autoprefixer": "^8.0.0",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-cssnano": "^2.1.3",
|
||||
"gulp-ejs": "^5.1.0",
|
||||
"gulp-sass": "^5.1.0",
|
||||
"gulp-serve": "^1.4.0",
|
||||
"gulp-sourcemaps": "^3.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"sass": "^1.63.3",
|
||||
"moment": "^2.30.1",
|
||||
"sass": "^1.75.0",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -1,255 +0,0 @@
|
||||
/*! =======================================================
|
||||
VERSION 6.0.12
|
||||
========================================================= */
|
||||
/*! =========================================================
|
||||
* bootstrap-slider.js
|
||||
*
|
||||
* Maintainers:
|
||||
* Kyle Kemp
|
||||
* - Twitter: @seiyria
|
||||
* - Github: seiyria
|
||||
* Rohit Kalkur
|
||||
* - Twitter: @Rovolutionary
|
||||
* - Github: rovolution
|
||||
*
|
||||
* =========================================================
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* ========================================================= */
|
||||
.slider {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
}
|
||||
.slider.slider-horizontal {
|
||||
width: 210px;
|
||||
height: 20px;
|
||||
}
|
||||
.slider.slider-horizontal .slider-track {
|
||||
height: 10px;
|
||||
width: 100%;
|
||||
margin-top: -5px;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
}
|
||||
.slider.slider-horizontal .slider-selection,
|
||||
.slider.slider-horizontal .slider-track-low,
|
||||
.slider.slider-horizontal .slider-track-high {
|
||||
height: 100%;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.slider.slider-horizontal .slider-tick,
|
||||
.slider.slider-horizontal .slider-handle {
|
||||
margin-left: -10px;
|
||||
margin-top: -5px;
|
||||
}
|
||||
.slider.slider-horizontal .slider-tick.triangle,
|
||||
.slider.slider-horizontal .slider-handle.triangle {
|
||||
border-width: 0 10px 10px 10px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-bottom-color: #0480be;
|
||||
margin-top: 0;
|
||||
}
|
||||
.slider.slider-horizontal .slider-tick-label-container {
|
||||
white-space: nowrap;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.slider.slider-horizontal .slider-tick-label-container .slider-tick-label {
|
||||
padding-top: 4px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
.slider.slider-vertical {
|
||||
height: 210px;
|
||||
width: 20px;
|
||||
}
|
||||
.slider.slider-vertical .slider-track {
|
||||
width: 10px;
|
||||
height: 100%;
|
||||
margin-left: -5px;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
}
|
||||
.slider.slider-vertical .slider-selection {
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.slider.slider-vertical .slider-track-low,
|
||||
.slider.slider-vertical .slider-track-high {
|
||||
width: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
.slider.slider-vertical .slider-tick,
|
||||
.slider.slider-vertical .slider-handle {
|
||||
margin-left: -5px;
|
||||
margin-top: -10px;
|
||||
}
|
||||
.slider.slider-vertical .slider-tick.triangle,
|
||||
.slider.slider-vertical .slider-handle.triangle {
|
||||
border-width: 10px 0 10px 10px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
border-left-color: #0480be;
|
||||
margin-left: 0;
|
||||
}
|
||||
.slider.slider-vertical .slider-tick-label-container {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.slider.slider-vertical .slider-tick-label-container .slider-tick-label {
|
||||
padding-left: 4px;
|
||||
}
|
||||
.slider.slider-disabled .slider-handle {
|
||||
background-image: -webkit-linear-gradient(top, #dfdfdf 0%, #bebebe 100%);
|
||||
background-image: -o-linear-gradient(top, #dfdfdf 0%, #bebebe 100%);
|
||||
background-image: linear-gradient(to bottom, #dfdfdf 0%, #bebebe 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdfdfdf', endColorstr='#ffbebebe', GradientType=0);
|
||||
}
|
||||
.slider.slider-disabled .slider-track {
|
||||
background-image: -webkit-linear-gradient(top, #e5e5e5 0%, #e9e9e9 100%);
|
||||
background-image: -o-linear-gradient(top, #e5e5e5 0%, #e9e9e9 100%);
|
||||
background-image: linear-gradient(to bottom, #e5e5e5 0%, #e9e9e9 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe5e5e5', endColorstr='#ffe9e9e9', GradientType=0);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.slider input {
|
||||
display: none;
|
||||
}
|
||||
.slider .tooltip.top {
|
||||
margin-top: -36px;
|
||||
}
|
||||
.slider .tooltip-inner {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.slider .hide {
|
||||
display: none;
|
||||
}
|
||||
.slider-track {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #f9f9f9 100%);
|
||||
background-image: -o-linear-gradient(top, #f5f5f5 0%, #f9f9f9 100%);
|
||||
background-image: linear-gradient(to bottom, #f5f5f5 0%, #f9f9f9 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0);
|
||||
-webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.slider-selection {
|
||||
position: absolute;
|
||||
background-image: -webkit-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
|
||||
background-image: -o-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
|
||||
background-image: linear-gradient(to bottom, #f9f9f9 0%, #f5f5f5 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9', endColorstr='#fff5f5f5', GradientType=0);
|
||||
-webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
||||
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.slider-selection.tick-slider-selection {
|
||||
background-image: -webkit-linear-gradient(top, #89cdef 0%, #81bfde 100%);
|
||||
background-image: -o-linear-gradient(top, #89cdef 0%, #81bfde 100%);
|
||||
background-image: linear-gradient(to bottom, #89cdef 0%, #81bfde 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef', endColorstr='#ff81bfde', GradientType=0);
|
||||
}
|
||||
.slider-track-low,
|
||||
.slider-track-high {
|
||||
position: absolute;
|
||||
background: transparent;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.slider-handle {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: #337ab7;
|
||||
background-image: -webkit-linear-gradient(top, #149bdf 0%, #0480be 100%);
|
||||
background-image: -o-linear-gradient(top, #149bdf 0%, #0480be 100%);
|
||||
background-image: linear-gradient(to bottom, #149bdf 0%, #0480be 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0);
|
||||
filter: none;
|
||||
-webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
|
||||
border: 0px solid transparent;
|
||||
}
|
||||
.slider-handle.round {
|
||||
border-radius: 50%;
|
||||
}
|
||||
.slider-handle.triangle {
|
||||
background: transparent none;
|
||||
}
|
||||
.slider-handle.custom {
|
||||
background: transparent none;
|
||||
}
|
||||
.slider-handle.custom::before {
|
||||
line-height: 20px;
|
||||
font-size: 20px;
|
||||
content: '\2605';
|
||||
color: #726204;
|
||||
}
|
||||
.slider-tick {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-image: -webkit-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
|
||||
background-image: -o-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
|
||||
background-image: linear-gradient(to bottom, #f9f9f9 0%, #f5f5f5 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9', endColorstr='#fff5f5f5', GradientType=0);
|
||||
-webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
||||
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
filter: none;
|
||||
opacity: 0.8;
|
||||
border: 0px solid transparent;
|
||||
}
|
||||
.slider-tick.round {
|
||||
border-radius: 50%;
|
||||
}
|
||||
.slider-tick.triangle {
|
||||
background: transparent none;
|
||||
}
|
||||
.slider-tick.custom {
|
||||
background: transparent none;
|
||||
}
|
||||
.slider-tick.custom::before {
|
||||
line-height: 20px;
|
||||
font-size: 20px;
|
||||
content: '\2605';
|
||||
color: #726204;
|
||||
}
|
||||
.slider-tick.in-selection {
|
||||
background-image: -webkit-linear-gradient(top, #89cdef 0%, #81bfde 100%);
|
||||
background-image: -o-linear-gradient(top, #89cdef 0%, #81bfde 100%);
|
||||
background-image: linear-gradient(to bottom, #89cdef 0%, #81bfde 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef', endColorstr='#ff81bfde', GradientType=0);
|
||||
opacity: 1;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-232
@@ -1,232 +0,0 @@
|
||||
(function(factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define(['angular', 'bootstrap-slider'], factory);
|
||||
} else if (typeof module === 'object' && module.exports) {
|
||||
module.exports = factory(require('angular'), require('bootstrap-slider'));
|
||||
} else if (window) {
|
||||
factory(window.angular, window.Slider);
|
||||
}
|
||||
})(function (angular, Slider) {
|
||||
|
||||
angular.module('ui.bootstrap-slider', [])
|
||||
.directive('slider', ['$parse', '$timeout', '$rootScope', function ($parse, $timeout, $rootScope) {
|
||||
return {
|
||||
restrict: 'AE',
|
||||
replace: true,
|
||||
template: '<div><input class="slider-input" type="text" style="width:100%" /></div>',
|
||||
require: 'ngModel',
|
||||
scope: {
|
||||
max: "=",
|
||||
min: "=",
|
||||
step: "=",
|
||||
value: "=",
|
||||
ngModel: '=',
|
||||
ngDisabled: '=',
|
||||
range: '=',
|
||||
sliderid: '=',
|
||||
ticks: '=',
|
||||
ticksLabels: '=',
|
||||
ticksSnapBounds: '=',
|
||||
ticksPositions: '=',
|
||||
scale: '=',
|
||||
focus: '=',
|
||||
formatter: '&',
|
||||
onStartSlide: '&',
|
||||
onStopSlide: '&',
|
||||
onSlide: '&'
|
||||
},
|
||||
link: function ($scope, element, attrs, ngModelCtrl, $compile) {
|
||||
var ngModelDeregisterFn, ngDisabledDeregisterFn;
|
||||
|
||||
var slider = initSlider();
|
||||
|
||||
function initSlider() {
|
||||
var options = {};
|
||||
|
||||
function setOption(key, value, defaultValue) {
|
||||
options[key] = value || defaultValue;
|
||||
}
|
||||
|
||||
function setFloatOption(key, value, defaultValue) {
|
||||
options[key] = value || value === 0 ? parseFloat(value) : defaultValue;
|
||||
}
|
||||
|
||||
function setBooleanOption(key, value, defaultValue) {
|
||||
options[key] = value ? value + '' === 'true' : defaultValue;
|
||||
}
|
||||
|
||||
function getArrayOrValue(value) {
|
||||
return (angular.isString(value) && value.indexOf("[") === 0) ? angular.fromJson(value) : value;
|
||||
}
|
||||
|
||||
setOption('id', $scope.sliderid);
|
||||
setOption('orientation', attrs.orientation, 'horizontal');
|
||||
setOption('selection', attrs.selection, 'before');
|
||||
setOption('handle', attrs.handle, 'round');
|
||||
setOption('tooltip', attrs.sliderTooltip || attrs.tooltip, 'show');
|
||||
setOption('tooltip_position', attrs.sliderTooltipPosition, 'top');
|
||||
setOption('tooltipseparator', attrs.tooltipseparator, ':');
|
||||
setOption('ticks', $scope.ticks);
|
||||
setOption('ticks_labels', $scope.ticksLabels);
|
||||
setOption('ticks_snap_bounds', $scope.ticksSnapBounds);
|
||||
setOption('ticks_positions', $scope.ticksPositions);
|
||||
setOption('scale', $scope.scale, 'linear');
|
||||
setOption('focus', $scope.focus);
|
||||
|
||||
setFloatOption('min', $scope.min, 0);
|
||||
setFloatOption('max', $scope.max, 10);
|
||||
setFloatOption('step', $scope.step, 1);
|
||||
var strNbr = options.step + '';
|
||||
var dotPos = strNbr.search(/[^.,]*$/);
|
||||
var decimals = strNbr.substring(dotPos);
|
||||
setFloatOption('precision', attrs.precision, decimals.length);
|
||||
|
||||
setBooleanOption('tooltip_split', attrs.tooltipsplit, false);
|
||||
setBooleanOption('enabled', attrs.enabled, true);
|
||||
setBooleanOption('naturalarrowkeys', attrs.naturalarrowkeys, false);
|
||||
setBooleanOption('reversed', attrs.reversed, false);
|
||||
|
||||
setBooleanOption('range', $scope.range, false);
|
||||
if (options.range) {
|
||||
if (angular.isArray($scope.value)) {
|
||||
options.value = $scope.value;
|
||||
}
|
||||
else if (angular.isString($scope.value)) {
|
||||
options.value = getArrayOrValue($scope.value);
|
||||
if (!angular.isArray(options.value)) {
|
||||
var value = parseFloat($scope.value);
|
||||
if (isNaN(value)) value = 5;
|
||||
|
||||
if (value < $scope.min) {
|
||||
value = $scope.min;
|
||||
options.value = [value, options.max];
|
||||
}
|
||||
else if (value > $scope.max) {
|
||||
value = $scope.max;
|
||||
options.value = [options.min, value];
|
||||
}
|
||||
else {
|
||||
options.value = [options.min, options.max];
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
options.value = [options.min, options.max]; // This is needed, because of value defined at $.fn.slider.defaults - default value 5 prevents creating range slider
|
||||
}
|
||||
$scope.ngModel = options.value; // needed, otherwise turns value into [null, ##]
|
||||
}
|
||||
else {
|
||||
setFloatOption('value', $scope.value, 5);
|
||||
}
|
||||
|
||||
if (attrs.formatter) {
|
||||
options.formatter = function(value) {
|
||||
return $scope.formatter({value: value});
|
||||
}
|
||||
}
|
||||
|
||||
// check if slider jQuery plugin exists
|
||||
if ('$' in window && $.fn.slider) {
|
||||
// adding methods to jQuery slider plugin prototype
|
||||
$.fn.slider.constructor.prototype.disable = function () {
|
||||
this.picker.off();
|
||||
};
|
||||
$.fn.slider.constructor.prototype.enable = function () {
|
||||
this.picker.on();
|
||||
};
|
||||
}
|
||||
|
||||
// destroy previous slider to reset all options
|
||||
if (element[0].__slider)
|
||||
element[0].__slider.destroy();
|
||||
|
||||
var slider = new Slider(element[0].getElementsByClassName('slider-input')[0], options);
|
||||
element[0].__slider = slider;
|
||||
|
||||
// everything that needs slider element
|
||||
var updateEvent = getArrayOrValue(attrs.updateevent);
|
||||
if (angular.isString(updateEvent)) {
|
||||
// if only single event name in string
|
||||
updateEvent = [updateEvent];
|
||||
}
|
||||
else {
|
||||
// default to slide event
|
||||
updateEvent = ['slide'];
|
||||
}
|
||||
angular.forEach(updateEvent, function (sliderEvent) {
|
||||
slider.on(sliderEvent, function (ev) {
|
||||
ngModelCtrl.$setViewValue(ev);
|
||||
});
|
||||
});
|
||||
slider.on('change', function (ev) {
|
||||
ngModelCtrl.$setViewValue(ev.newValue);
|
||||
});
|
||||
|
||||
|
||||
// Event listeners
|
||||
var sliderEvents = {
|
||||
slideStart: 'onStartSlide',
|
||||
slide: 'onSlide',
|
||||
slideStop: 'onStopSlide'
|
||||
};
|
||||
angular.forEach(sliderEvents, function (sliderEventAttr, sliderEvent) {
|
||||
var fn = $parse(attrs[sliderEventAttr]);
|
||||
slider.on(sliderEvent, function (ev) {
|
||||
if ($scope[sliderEventAttr]) {
|
||||
$scope.$apply(function () {
|
||||
fn($scope.$parent, { $event: ev, value: ev });
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// deregister ngDisabled watcher to prevent memory leaks
|
||||
if (angular.isFunction(ngDisabledDeregisterFn)) {
|
||||
ngDisabledDeregisterFn();
|
||||
ngDisabledDeregisterFn = null;
|
||||
}
|
||||
|
||||
ngDisabledDeregisterFn = $scope.$watch('ngDisabled', function (value) {
|
||||
if (value) {
|
||||
slider.disable();
|
||||
}
|
||||
else {
|
||||
slider.enable();
|
||||
}
|
||||
});
|
||||
|
||||
// deregister ngModel watcher to prevent memory leaks
|
||||
if (angular.isFunction(ngModelDeregisterFn)) ngModelDeregisterFn();
|
||||
ngModelDeregisterFn = $scope.$watch('ngModel', function (value) {
|
||||
if($scope.range){
|
||||
slider.setValue(value);
|
||||
}else{
|
||||
slider.setValue(parseFloat(value));
|
||||
}
|
||||
slider.relayout();
|
||||
}, true);
|
||||
|
||||
return slider;
|
||||
}
|
||||
|
||||
|
||||
var watchers = ['min', 'max', 'step', 'range', 'scale', 'ticksLabels', 'ticks'];
|
||||
angular.forEach(watchers, function (prop) {
|
||||
$scope.$watch(prop, function () {
|
||||
slider = initSlider();
|
||||
});
|
||||
});
|
||||
|
||||
var globalEvents = ['relayout', 'refresh', 'resize'];
|
||||
angular.forEach(globalEvents, function(event) {
|
||||
if(angular.isFunction(slider[event])) {
|
||||
$scope.$on('slider:' + event, function () {
|
||||
slider[event]();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}])
|
||||
;
|
||||
});
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
// !!!
|
||||
// This module is manually patched by us to not only report valid domains, but verify that subdomains are not accepted
|
||||
// !!!
|
||||
'use strict';
|
||||
|
||||
angular.module('ngTld', [])
|
||||
.factory('ngTld', ngTld)
|
||||
.directive('checkTld', checkTld);
|
||||
|
||||
function ngTld() {
|
||||
function isValid(path) {
|
||||
// https://github.com/oncletom/tld.js/issues/58
|
||||
return (path.slice(-1) !== '.') && tld.isValid(path);
|
||||
}
|
||||
|
||||
function tldExists(path) {
|
||||
return (path.slice(-1) !== '.') && path === tld.getDomain(path);
|
||||
}
|
||||
|
||||
function isSubdomain(path) {
|
||||
return (path.slice(-1) !== '.') && !!tld.getDomain(path) && path !== tld.getDomain(path);
|
||||
}
|
||||
|
||||
function isNakedDomain(path) {
|
||||
return (path.slice(-1) !== '.') && !!tld.getDomain(path) && path === tld.getDomain(path);
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: isValid,
|
||||
tldExists: tldExists,
|
||||
isSubdomain: isSubdomain,
|
||||
isNakedDomain: isNakedDomain
|
||||
};
|
||||
}
|
||||
|
||||
function checkTld(ngTld) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
link: function(scope, element, attr, ngModel) {
|
||||
ngModel.$validators.invalidTld = function(modelValue, viewValue) {
|
||||
return ngTld.tldExists(ngModel.$viewValue.toLowerCase());
|
||||
};
|
||||
|
||||
ngModel.$validators.invalidSubdomain = function(modelValue, viewValue) {
|
||||
return !ngTld.isSubdomain(ngModel.$viewValue.toLowerCase());
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
Vendored
-1913
File diff suppressed because one or more lines are too long
@@ -0,0 +1,155 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<title>Cloudron Setup</title>
|
||||
<meta name="description" content="Cloudron Setup">
|
||||
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css">
|
||||
|
||||
<!-- Fontawesome -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.min.css?<%= revision %>"/>
|
||||
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js"></script>
|
||||
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js"></script>
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js"></script>
|
||||
|
||||
<!-- Angularjs scripts -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/autofill-event.js"></script>
|
||||
|
||||
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js"></script>
|
||||
|
||||
<!-- Angular translate https://angular-translate.github.io/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-loader-static-files.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-cookie.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-local.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Setup Application -->
|
||||
<script type="text/javascript" src="/js/activation.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="setup" ng-app="Application" ng-controller="SetupController">
|
||||
|
||||
<a class="offline-banner animateMe" ng-show="client.offline" ng-cloak href="https://docs.cloudron.io/troubleshooting/" target="_blank"><i class="fa fa-circle-notch fa-spin"></i> Cloudron is offline. Reconnecting...</a>
|
||||
|
||||
<div class="main-container" ng-show="initialized">
|
||||
<div class="row" ng-show="view === 'owner'">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="card" style="max-width: none; padding: 20px;">
|
||||
<form role="form" name="ownerForm" ng-submit="owner.submit()" novalidate>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<h1>Welcome to Cloudron</h1>
|
||||
<h3>Set up Admin Account</h3>
|
||||
<p class="has-error text-center" ng-show="owner.error.generic">{{ owner.error.generic }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<div class="form-group" ng-class="{ 'has-error': ownerForm.displayName.$dirty && ownerForm.displayName.$invalid }">
|
||||
<label class="control-label" for="inputDisplayName">Full Name</label>
|
||||
<input type="text" class="form-control" ng-model="owner.displayName" id="inputDisplayName" name="displayName" placeholder="Full Name" required autocomplete="off" ng-disabled="owner.busy" autofocus>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (ownerForm.email.$dirty && ownerForm.email.$invalid) || (!ownerForm.email.$dirty && owner.error.email) }">
|
||||
<label class="control-label" for="inputEmail">Email <sup><a ng-href="https://docs.cloudron.io/installation/#admin-account" class="help" target="_blank" tabIndex="-1"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<input type="email" class="form-control" ng-model="owner.email" id="inputEmail" name="email" placeholder="Email" required autocomplete="off" ng-disabled="owner.busy">
|
||||
<small>A valid email is required for Let's Encrypt certificates. This email is local to your Cloudron. </small>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (ownerForm.username.$dirty && ownerForm.username.$invalid) || (!ownerForm.username.$dirty && owner.error.username) }">
|
||||
<label class="control-label" for="inputUsername">Username</label>
|
||||
<input type="text" class="form-control" ng-model="owner.username" id="inputUsername" name="username" placeholder="Username" ng-maxlength="512" ng-minlength="1" required autocomplete="off" ng-disabled="owner.busy">
|
||||
<small>{{ owner.error.username }}</small>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 0;" ng-class="{ 'has-error': ownerForm.password.$dirty && ownerForm.password.$invalid }">
|
||||
<label class="control-label" for="inputPassword">Password</label>
|
||||
<input type="password" class="form-control" ng-model="owner.password" id="inputPassword" name="password" placeholder="Password" ng-pattern="/^.{8,}$/" required autocomplete="off" ng-disabled="owner.busy" password-reveal>
|
||||
<small><span ng-show="ownerForm.password.$dirty && ownerForm.password.$invalid">Password must be at least 8 characters</span> </small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<button type="submit" class="btn btn-success" ng-disabled="ownerForm.$invalid || owner.busy"><i class="fa fa-circle-notch fa-spin" ng-show="owner.busy"></i> Create Admin</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="view === 'finished'">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="card" style="max-width: none; padding: 20px 40px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<h1>Cloudron is ready to use</h1>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
Before you start:
|
||||
<ul class="fa-ul">
|
||||
<li><i class="fa-li fa fa-users"></i>
|
||||
<b>User management</b>: Cloudron has a central user directory. When installing an app,
|
||||
you can set it up to authenticate against this directory.
|
||||
</li>
|
||||
<br/>
|
||||
<li><i class="fa-li fa fa-envelope-open"></i>
|
||||
<b>Email Configuration</b>: Apps are configured to send email based on the settings in the Email view.
|
||||
This saves you the trouble of having to configure mail settings inside each app.
|
||||
</li>
|
||||
<br/>
|
||||
<li><i class="fa-li fa fa-archive"></i>
|
||||
<b>Backups</b>: Store your backups on storage services completely independent from your server.
|
||||
You can use backups to seamlessly migrate your setup to another server.
|
||||
</li>
|
||||
<br/>
|
||||
<li><i class="fa-li fa fa-birthday-cake"></i>
|
||||
<b>Updates</b>: The Cloudron team tracks upstream releases and publishes app updates after testing.
|
||||
Your apps are kept fresh & secure.
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<a class="btn btn-success" ng-href="firstTimeLoginUrl">Proceed to Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted">©2022 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted"><a href="https://forum.cloudron.io" target="_blank">Forum <i class="fa fa-comments"></i></a></span>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -23,7 +23,7 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-family: "Roboto","Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||
font-family: "Noto Sans", Helvetica, Arial, sans-serif;
|
||||
line-height: 1.846;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,11 @@ tmp.forEach(function (pair) {
|
||||
if (pair.indexOf('access_token=') === 0) localStorage.token = pair.split('=')[1];
|
||||
});
|
||||
|
||||
window.location.href = '/';
|
||||
var redirectTo = '/';
|
||||
if (localStorage.getItem('redirectToHash')) {
|
||||
redirectTo += localStorage.getItem('redirectToHash');
|
||||
localStorage.removeItem('redirectToHash');
|
||||
}
|
||||
window.location.href = redirectTo;
|
||||
|
||||
</script>
|
||||
|
||||
@@ -15,11 +15,10 @@
|
||||
<!-- CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/slick.css?<%= revision %>"/>
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.css?<%= revision %>"/>
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/bootstrap-slider/bootstrap-slider.min.css?<%= revision %>"/>
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css?<%= revision %>">
|
||||
|
||||
<!-- Fontawesome -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.css?<%= revision %>"/>
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.min.css?<%= revision %>"/>
|
||||
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js?<%= revision %>"></script>
|
||||
@@ -50,10 +49,6 @@
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-fittext.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/autofill-event.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Angular directives for tldjs -->
|
||||
<script type="text/javascript" src="/3rdparty/js/tld.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-tld.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js?<%= revision %>"></script>
|
||||
|
||||
@@ -73,10 +68,6 @@
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Bootstrap slider -->
|
||||
<script type="text/javascript" src="/3rdparty/bootstrap-slider/bootstrap-slider.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/bootstrap-slider/slider.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Anugular Multiselect https://github.com/sebastianha/angular-bootstrap-multiselect -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-bootstrap-multiselect.js?<%= revision %>"></script>
|
||||
|
||||
@@ -177,7 +168,7 @@
|
||||
<li><a href="#/profile"><i class="fa fa-user fa-fw"></i> {{ 'profile.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastMailManager" class="divider"></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/backups"><i class="fa fa-archive fa-fw"></i> {{ 'backups.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastOwner"><a href="#/branding"><i class="fa fa-passport fa-fw"></i> {{ 'branding.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/branding"><i class="fa fa-passport fa-fw"></i> {{ 'branding.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/domains"><i class="fa fa-globe fa-fw"></i> {{ 'domains.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastMailManager"><a href="#/email"><i class="fa fa-envelope fa-fw"></i> {{ 'emails.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/eventlog"><i class="fa fa-list-alt fa-fw"></i> {{ 'eventlog.title' | tr }}</a></li>
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular, window, document, localStorage, redirectIfNeeded */
|
||||
/* global $ */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification', 'ui.bootstrap']);
|
||||
|
||||
app.controller('SetupController', ['$scope', 'Client', function ($scope, Client) {
|
||||
// Stupid angular location provider either wants html5 location mode or not, do the query parsing on my own
|
||||
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
$scope.client = Client;
|
||||
$scope.view = '';
|
||||
$scope.initialized = false;
|
||||
$scope.setupToken = '';
|
||||
$scope.firstTimeLoginUrl = '';
|
||||
|
||||
$scope.owner = {
|
||||
error: null,
|
||||
busy: false,
|
||||
|
||||
email: '',
|
||||
displayName: '',
|
||||
username: '',
|
||||
password: '',
|
||||
|
||||
submit: function () {
|
||||
$scope.owner.busy = true;
|
||||
$scope.owner.error = null;
|
||||
|
||||
var data = {
|
||||
username: $scope.owner.username,
|
||||
password: $scope.owner.password,
|
||||
email: $scope.owner.email,
|
||||
displayName: $scope.owner.displayName,
|
||||
setupToken: $scope.setupToken
|
||||
};
|
||||
|
||||
Client.createAdmin(data, function (error, autoLoginToken) {
|
||||
if (error && error.statusCode === 400) {
|
||||
$scope.owner.busy = false;
|
||||
|
||||
if (error.message === 'Invalid email') {
|
||||
$scope.owner.error = { email: error.message };
|
||||
$scope.owner.email = '';
|
||||
$scope.ownerForm.email.$setPristine();
|
||||
setTimeout(function () { $('#inputEmail').focus(); }, 200);
|
||||
} else {
|
||||
$scope.owner.error = { username: error.message };
|
||||
$scope.owner.username = '';
|
||||
$scope.ownerForm.username.$setPristine();
|
||||
setTimeout(function () { $('#inputUsername').focus(); }, 200);
|
||||
}
|
||||
return;
|
||||
} else if (error) {
|
||||
$scope.owner.busy = false;
|
||||
console.error('Internal error', error);
|
||||
$scope.owner.error = { generic: error.message };
|
||||
return;
|
||||
}
|
||||
|
||||
// set token to autologin on first oidc flow
|
||||
localStorage.cloudronFirstTimeToken = autoLoginToken;
|
||||
|
||||
$scope.firstTimeLoginUrl = '/openid/auth?client_id=cid-webadmin&scope=openid email profile&response_type=code token&redirect_uri=' + window.location.origin + '/authcallback.html';
|
||||
|
||||
setView('finished');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function setView(view) {
|
||||
if (view === 'finished') {
|
||||
$scope.view = 'finished';
|
||||
} else {
|
||||
$scope.view = 'owner';
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
if (redirectIfNeeded(status, 'activation')) return; // redirected to some other view...
|
||||
|
||||
setView(search.view);
|
||||
|
||||
$scope.setupToken = search.setupToken;
|
||||
$scope.initialized = true;
|
||||
|
||||
// Ensure we have a good autofocus
|
||||
setTimeout(function () {
|
||||
$(document).find("[autofocus]:first").focus();
|
||||
}, 250);
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
}]);
|
||||
+309
-118
@@ -195,22 +195,47 @@ const REGIONS_SCALEWAY = [
|
||||
{ name: 'Warsaw (PL-WAW)', value: 'https://s3.pl-waw.scw.cloud', region: 'nl-ams' }
|
||||
];
|
||||
|
||||
// https://www.linode.com/docs/products/storage/object-storage/guides/urls/
|
||||
const REGIONS_LINODE = [
|
||||
{ name: 'Atlanta', value: 'us-southeast-1.linodeobjects.com', region: 'us-east-1' }, // default
|
||||
{ name: 'Newark', value: 'us-east-1.linodeobjects.com', region: 'us-east-1' },
|
||||
{ name: 'Frankfurt', value: 'eu-central-1.linodeobjects.com', region: 'us-east-1' },
|
||||
{ name: 'Singapore', value: 'ap-south-1.linodeobjects.com', region: 'us-east-1' }
|
||||
{ name: 'Amsterdam', value: 'https://nl-ams-1.linodeobjects.com', region: 'nl-ams-1' },
|
||||
{ name: 'Atlanta', value: 'https://us-southeast-1.linodeobjects.com', region: 'us-southeast-1' },
|
||||
{ name: 'Chennai', value: 'https://in-maa-1.linodeobjects.com', region: 'in-maa-1' },
|
||||
{ name: 'Chicago', value: 'https://us-ord-1.linodeobjects.com', region: 'us-ord-1' },
|
||||
{ name: 'Frankfurt', value: 'https://eu-central-1.linodeobjects.com', region: 'eu-central-1' },
|
||||
{ name: 'Jakarta', value: 'https://id-cgk-1.linodeobjects.com', region: 'id-cgk-1' },
|
||||
{ name: 'Los Angeles, CA (USA)', value: 'https://us-lax-1.linodeobjects.com', region: 'us-lax-1' },
|
||||
{ name: 'Miami', value: 'https://us-mia-1.linodeobjects.com', region: 'us-mia-1' },
|
||||
{ name: 'Milan', value: 'https://it-mil-1.linodeobjects.com', region: 'it-mil-1' },
|
||||
{ name: 'Newark', value: 'https://us-east-1.linodeobjects.com', region: 'us-east-1' }, // default
|
||||
{ name: 'Osaka', value: 'https://jp-osa-1.linodeobjects.com', region: 'jp-osa-1' },
|
||||
{ name: 'Paris', value: 'https://fr-par-1.linodeobjects.com', region: 'fr-par-1' },
|
||||
{ name: 'Sao Paulo', value: 'https://br-gru-1.linodeobjects.com', region: 'br-gru-1' },
|
||||
{ name: 'Seattle', value: 'https://us-sea-1.linodeobjects.com', region: 'us-sea-1' },
|
||||
{ name: 'Singapore', value: 'https://ap-south-1.linodeobjects.com', region: 'ap-south-1' },
|
||||
{ name: 'Stockholm', value: 'https://se-sto-1.linodeobjects.com', region: 'se-sto-1' },
|
||||
{ name: 'Washington', value: 'https://us-iad-1.linodeobjects.com', region: 'us-iad-1' },
|
||||
];
|
||||
|
||||
// note: ovh also has a storage endpoint but that only supports path style access (https://docs.ovh.com/au/en/storage/object-storage/s3/location/)
|
||||
const REGIONS_OVH = [
|
||||
{ name: 'Beauharnois (BHS)', value: 'https://s3.bhs.cloud.ovh.net', region: 'bhs' }, // default
|
||||
{ name: 'Frankfurt (DE)', value: 'https://s3.de.cloud.ovh.net', region: 'de' },
|
||||
{ name: 'Gravelines (GRA)', value: 'https://s3.gra.cloud.ovh.net', region: 'gra' },
|
||||
{ name: 'Strasbourg (SBG)', value: 'https://s3.sbg.cloud.ovh.net', region: 'sbg' },
|
||||
{ name: 'London (UK)', value: 'https://s3.uk.cloud.ovh.net', region: 'uk' },
|
||||
{ name: 'Sydney (SYD)', value: 'https://s3.syd.cloud.ovh.net', region: 'syd' },
|
||||
{ name: 'Warsaw (WAW)', value: 'https://s3.waw.cloud.ovh.net', region: 'waw' },
|
||||
{ name: 'Beauharnois (BHS)', value: 'https://s3.bhs.io.cloud.ovh.net', region: 'bhs' }, // default
|
||||
{ name: 'Frankfurt (DE)', value: 'https://s3.de.io.cloud.ovh.net', region: 'de' },
|
||||
{ name: 'Gravelines (GRA)', value: 'https://s3.gra.io.cloud.ovh.net', region: 'gra' },
|
||||
{ name: 'Roubaix (RBX)', value: 'https://s3.rbx.io.cloud.ovh.net', region: 'rbx' },
|
||||
{ name: 'Strasbourg (SBG)', value: 'https://s3.sbg.io.cloud.ovh.net', region: 'sbg' },
|
||||
{ name: 'London (UK)', value: 'https://s3.uk.io.cloud.ovh.net', region: 'uk' },
|
||||
{ name: 'Sydney (SYD)', value: 'https://s3.syd.io.cloud.ovh.net', region: 'syd' },
|
||||
{ name: 'Warsaw (WAW)', value: 'https://s3.waw.io.cloud.ovh.net', region: 'waw' },
|
||||
];
|
||||
|
||||
const ENDPOINTS_OVH = [
|
||||
{ name: 'OVH Europe', value: 'ovh-eu' },
|
||||
{ name: 'OVH US', value: 'ovh-us' },
|
||||
{ name: 'OVH North-America', value: 'ovh-ca' },
|
||||
{ name: 'SoYouStart Europe', value: 'soyoustart-eu' },
|
||||
{ name: 'SoYouStart North-America', value: 'soyoustart-ca' },
|
||||
{ name: 'Kimsufi Europe', value: 'kimsufi-eu' },
|
||||
{ name: 'Kimsufi North-America', value: 'kimsufi-ca' },
|
||||
];
|
||||
|
||||
// https://docs.ionos.com/cloud/managed-services/s3-object-storage/endpoints
|
||||
@@ -303,7 +328,7 @@ function prettyBinarySize(size, fallback) {
|
||||
|
||||
// we can also use KB here (JEDEC)
|
||||
var i = Math.floor(Math.log(size) / Math.log(1024));
|
||||
return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'KiB', 'MiB', 'GiB', 'TiB'][i];
|
||||
return (size / Math.pow(1024, i)).toFixed(3) * 1 + ' ' + ['B', 'KiB', 'MiB', 'GiB', 'TiB'][i];
|
||||
}
|
||||
|
||||
// decimal units (SI) 1000 based
|
||||
@@ -341,6 +366,8 @@ angular.module('Application').filter('trKeyFromPeriod', function () {
|
||||
angular.module('Application').filter('prettyDate', function ($translate) {
|
||||
// http://ejohn.org/files/pretty.js
|
||||
return function prettyDate(utc) {
|
||||
if (utc === null) return $translate.instant('main.prettyDate.never', {});
|
||||
|
||||
var date = new Date(utc), // this converts utc into browser timezone and not cloudron timezone!
|
||||
diff = (((new Date()).getTime() - date.getTime()) / 1000) + 30, // add 30seconds for clock skew
|
||||
day_diff = Math.floor(diff / 86400);
|
||||
@@ -424,12 +451,81 @@ function translateFilterFactory($parse, $translate) {
|
||||
translateFilterFactory.displayName = 'translateFilterFactory';
|
||||
angular.module('Application').filter('tr', translateFilterFactory);
|
||||
|
||||
// checks provision status and redirects to correct view
|
||||
// {
|
||||
// setup: { active, message, errorMessage }
|
||||
// restore { active, message, errorMessage }
|
||||
// activated
|
||||
// adminFqn
|
||||
// }
|
||||
// returns true if redirected . currentView is one of dashboard/restore/setup/activation
|
||||
function redirectIfNeeded(status, currentView) {
|
||||
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
if ('develop' in search || localStorage.getItem('develop')) {
|
||||
console.warn('Cloudron develop mode on. To disable run localStorage.removeItem(\'develop\')');
|
||||
localStorage.setItem('develop', true);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(status, currentView);
|
||||
|
||||
if (status.activated) {
|
||||
console.log('Already activated');
|
||||
if (currentView === 'dashboard') {
|
||||
// support local development with localhost check
|
||||
if (window.location.hostname !== status.adminFqdn && window.location.hostname !== 'localhost' && !window.location.hostname.startsWith('192.')) {
|
||||
// user is accessing by IP or by the old admin location (pre-migration)
|
||||
window.location.href = '/setup.html' + window.location.search;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
window.location.href = 'https://' + status.adminFqdn + '/';
|
||||
return true;
|
||||
}
|
||||
|
||||
if (status.setup.active) {
|
||||
console.log('Setup is active');
|
||||
if (currentView === 'setup') return false;
|
||||
window.location.href = '/setup.html' + window.location.search;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (status.restore.active) {
|
||||
console.log('Restore is active');
|
||||
if (currentView === 'restore') return;
|
||||
window.location.href = '/restore.html' + window.location.search;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (status.adminFqdn) {
|
||||
console.log('adminFqdn is set');
|
||||
// if we are here from https://ip/activation.html ,go to https://admin/activation.html
|
||||
if (status.adminFqdn !== window.location.hostname) {
|
||||
window.location.href = 'https://' + status.adminFqdn + '/activation.html' + (window.location.search);
|
||||
return true;
|
||||
}
|
||||
if (currentView === 'activation') return false;
|
||||
window.location.href = 'https://' + status.adminFqdn + '/activation.html' + (window.location.search);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentView === 'dashboard') {
|
||||
window.location.href = '/setup.html' + window.location.search;
|
||||
return true;
|
||||
}
|
||||
|
||||
// if we are here, proceed with current view
|
||||
return false;
|
||||
}
|
||||
|
||||
// ----------------------------------------------
|
||||
// Cloudron REST API wrapper
|
||||
// ----------------------------------------------
|
||||
|
||||
angular.module('Application').service('Client', ['$http', '$interval', '$timeout', 'md5', 'Notification', function ($http, $interval, $timeout, md5, Notification) {
|
||||
angular.module('Application').service('Client', ['$http', '$interval', '$timeout', 'md5', 'Notification', '$translate', function ($http, $interval, $timeout, md5, Notification, $translate) {
|
||||
var client = null;
|
||||
|
||||
// variable available only here to avoid this._property pattern
|
||||
@@ -594,6 +690,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
twoFactorAuthenticationEnabled: false,
|
||||
source: null,
|
||||
avatarUrl: null,
|
||||
avatarType: null,
|
||||
hasBackgroundImage: false
|
||||
};
|
||||
this._config = {
|
||||
@@ -612,6 +709,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
// window.location fallback for websocket connections which do not have relative uris
|
||||
this.apiOrigin = '<%= apiOrigin %>' || window.location.origin;
|
||||
this.avatar = '';
|
||||
this.background = '';
|
||||
this._availableLanguages = ['en'];
|
||||
this._appstoreAppCache = [];
|
||||
|
||||
@@ -722,7 +820,8 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
this._userInfo.twoFactorAuthenticationEnabled = userInfo.twoFactorAuthenticationEnabled;
|
||||
this._userInfo.role = userInfo.role;
|
||||
this._userInfo.source = userInfo.source;
|
||||
this._userInfo.avatarUrl = userInfo.avatarUrl + '?s=128&default=mp&ts=' + Date.now(); // we add the timestamp to avoid caching
|
||||
this._userInfo.avatarUrl = userInfo.avatarUrl + '?ts=' + Date.now(); // we add the timestamp to avoid caching
|
||||
this._userInfo.avatarType = userInfo.avatarType;
|
||||
this._userInfo.hasBackgroundImage = userInfo.hasBackgroundImage;
|
||||
this._userInfo.isAtLeastOwner = [ ROLES.OWNER ].indexOf(userInfo.role) !== -1;
|
||||
this._userInfo.isAtLeastAdmin = [ ROLES.OWNER, ROLES.ADMIN ].indexOf(userInfo.role) !== -1;
|
||||
@@ -739,9 +838,6 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
this._config.consoleServerOrigin = '<%= appstore.consoleOrigin %>';
|
||||
<% } -%>
|
||||
|
||||
// => This is just for easier testing
|
||||
// this._config.features.externalLdap = false;
|
||||
|
||||
this._configListener.forEach(function (callback) {
|
||||
callback(that._config);
|
||||
});
|
||||
@@ -794,7 +890,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.userInfo = function (callback) {
|
||||
Client.prototype.getProfile = function (callback) {
|
||||
get('/api/v1/profile', null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
|
||||
@@ -802,6 +898,31 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.hasCloudronBackground = function (callback) {
|
||||
get('/api/v1/branding/cloudron_background', null, function (error, data, status) {
|
||||
if (error && error.statusCode !== 404) callback(error);
|
||||
else if (error) callback(null, false);
|
||||
else callback(null, status === 200);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.changeCloudronBackground = function (background, callback) {
|
||||
var fd = new FormData();
|
||||
if (background) fd.append('background', background);
|
||||
|
||||
var config = {
|
||||
headers: { 'Content-Type': undefined },
|
||||
transformRequest: angular.identity
|
||||
};
|
||||
|
||||
post('/api/v1/branding/cloudron_background', fd, config, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 202) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.changeCloudronAvatar = function (avatarFile, callback) {
|
||||
var fd = new FormData();
|
||||
fd.append('avatar', avatarFile);
|
||||
@@ -836,7 +957,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
subdomain: config.subdomain,
|
||||
domain: config.domain,
|
||||
secondaryDomains: config.secondaryDomains,
|
||||
portBindings: config.portBindings,
|
||||
ports: config.ports,
|
||||
accessRestriction: config.accessRestriction,
|
||||
cert: config.cert,
|
||||
key: config.key,
|
||||
@@ -858,7 +979,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
subdomain: config.subdomain,
|
||||
domain: config.domain,
|
||||
secondaryDomains: config.secondaryDomains,
|
||||
portBindings: config.portBindings,
|
||||
ports: config.ports,
|
||||
backupId: config.backupId,
|
||||
overwriteDns: !!config.overwriteDns
|
||||
};
|
||||
@@ -931,6 +1052,15 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.ackAppChecklistItem = function (appId, key, acknowledged, callback) {
|
||||
put('/api/v1/apps/' + appId + '/checklist/' + key, { done: acknowledged }, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 202) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.updateApp = function (id, manifest, options, callback) {
|
||||
var data = {
|
||||
appStoreId: manifest.id + '@' + manifest.version,
|
||||
@@ -1016,7 +1146,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
const storageConfig = Object.assign({}, backupConfig);
|
||||
delete storageConfig.limits;
|
||||
|
||||
post('/api/v1/backups/config/storage', backupConfig, null, function (error, data, status) {
|
||||
post('/api/v1/backups/config/storage', storageConfig, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
|
||||
@@ -1064,6 +1194,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
callback(null, data);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.remountBackupStorage = function (callback) {
|
||||
post('/api/v1/backups/remount', {}, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
@@ -1073,15 +1204,6 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.getSupportConfig = function (callback) {
|
||||
get('/api/v1/support/config', null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null, data);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.setExternalLdapConfig = function (config, callback) {
|
||||
post('/api/v1/external_ldap/config', config, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
@@ -1470,16 +1592,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.restore = function (backupConfig, remotePath, version, ipv4Config, skipDnsSetup, setupToken, callback) {
|
||||
var data = {
|
||||
backupConfig: backupConfig,
|
||||
remotePath: remotePath,
|
||||
version: version,
|
||||
ipv4Config: ipv4Config,
|
||||
skipDnsSetup: skipDnsSetup,
|
||||
setupToken: setupToken
|
||||
};
|
||||
|
||||
Client.prototype.restore = function (data, callback) {
|
||||
post('/api/v1/provision/restore', data, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status));
|
||||
@@ -1719,8 +1832,8 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.setGroups = function (userId, groupIds, callback) {
|
||||
put('/api/v1/users/' + userId + '/groups', { groupIds: groupIds }, null, function (error, data, status) {
|
||||
Client.prototype.setLocalGroups = function (userId, localGroupIds, callback) {
|
||||
put('/api/v1/users/' + userId + '/groups', { groupIds: localGroupIds }, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 204) return callback(new ClientError(status, data));
|
||||
|
||||
@@ -1750,12 +1863,12 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.updateGroup = function (id, name, callback) {
|
||||
Client.prototype.setGroupName = function (id, name, callback) {
|
||||
var data = {
|
||||
name: name
|
||||
};
|
||||
|
||||
post('/api/v1/groups/' + id, data, null, function (error, data, status) {
|
||||
put('/api/v1/groups/' + id + '/name', data, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
|
||||
@@ -1815,15 +1928,6 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.getAppLimits = function (appId, callback) {
|
||||
get('/api/v1/apps/' + appId + '/limits', null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null, data.limits);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.getAppWithTask = function (appId, callback) {
|
||||
var that = this;
|
||||
|
||||
@@ -1873,6 +1977,15 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.detectIp = function (callback) {
|
||||
post('/api/v1/provision/detect_ip', {}, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null, data);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.setup = function (data, callback) {
|
||||
post('/api/v1/provision/setup', data, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
@@ -1898,10 +2011,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
if (error) return callback(error);
|
||||
if (status !== 201) return callback(new ClientError(status, result));
|
||||
|
||||
that.setToken(result.token);
|
||||
that.setUserInfo({ username: data.username, email: data.email, admin: true, twoFactorAuthenticationEnabled: false, source: '', avatarUrl: null });
|
||||
|
||||
callback(null, result.activated);
|
||||
callback(null, result.token);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1956,11 +2066,9 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.addOidcClient = function (id, name, secret, loginRedirectUri, tokenSignatureAlgorithm, callback) {
|
||||
Client.prototype.addOidcClient = function (name, loginRedirectUri, tokenSignatureAlgorithm, callback) {
|
||||
var data = {
|
||||
id: id,
|
||||
name: name,
|
||||
secret: secret,
|
||||
loginRedirectUri: loginRedirectUri,
|
||||
tokenSignatureAlgorithm: tokenSignatureAlgorithm
|
||||
};
|
||||
@@ -1973,9 +2081,8 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.updateOidcClient = function (id, name, secret, loginRedirectUri, tokenSignatureAlgorithm, callback) {
|
||||
Client.prototype.updateOidcClient = function (id, name, loginRedirectUri, tokenSignatureAlgorithm, callback) {
|
||||
var data = {
|
||||
secret: secret,
|
||||
name: name,
|
||||
loginRedirectUri: loginRedirectUri,
|
||||
tokenSignatureAlgorithm, tokenSignatureAlgorithm
|
||||
@@ -2044,7 +2151,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
};
|
||||
|
||||
Client.prototype.isRebootRequired = function (callback) {
|
||||
get('/api/v1/system/reboot', null, function (error, data, status) {
|
||||
get('/api/v1/system/info', null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
|
||||
@@ -2070,15 +2177,6 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.disks = function (callback) {
|
||||
get('/api/v1/system/disks', null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null, data);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.diskUsage = function (callback) {
|
||||
get('/api/v1/system/disk_usage', null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
@@ -2097,6 +2195,24 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.systemInfo = function (callback) {
|
||||
get('/api/v1/system/info', null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null, data.info);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.cpus = function (callback) {
|
||||
get('/api/v1/system/cpus', null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null, data.cpus);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.memory = function (callback) {
|
||||
get('/api/v1/system/memory', null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
@@ -2186,7 +2302,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
// amend properties to mimick full app
|
||||
data.applinks.forEach(function (applink) {
|
||||
applink.type = APP_TYPES.LINK;
|
||||
applink.fqdn = applink.upstreamUri; // this fqdn may contain the protocol!
|
||||
applink.fqdn = applink.upstreamUri;
|
||||
applink.manifest = { addons: {}};
|
||||
applink.installationState = ISTATES.INSTALLED;
|
||||
applink.runState = RSTATES.RUNNING;
|
||||
@@ -2245,17 +2361,26 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.updateUser = function (user, callback) {
|
||||
var data = {
|
||||
email: user.email,
|
||||
displayName: user.displayName,
|
||||
fallbackEmail: user.fallbackEmail,
|
||||
active: user.active,
|
||||
role: user.role
|
||||
};
|
||||
if (user.username) data.username = user.username;
|
||||
Client.prototype.updateUserProfile = function (userId, data, callback) {
|
||||
post('/api/v1/users/' + userId + '/profile', data, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 204) return callback(new ClientError(status, data));
|
||||
|
||||
post('/api/v1/users/' + user.id, data, null, function (error, data, status) {
|
||||
callback(null);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.setRole = function (userId, role, callback) {
|
||||
put('/api/v1/users/' + userId + '/role', { role: role }, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 204) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.setActive = function (userId, active, callback) {
|
||||
put('/api/v1/users/' + userId + '/active', { active: active }, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 204) return callback(new ClientError(status, data));
|
||||
|
||||
@@ -2279,8 +2404,35 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.updateProfile = function (data, callback) {
|
||||
post('/api/v1/profile', data, null, function (error, data, status) {
|
||||
Client.prototype.setProfileDisplayName = function (displayName, callback) {
|
||||
post('/api/v1/profile/display_name', { displayName: displayName }, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 204) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null, data);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.setProfileLanguage = function (language, callback) {
|
||||
post('/api/v1/profile/language', { language: language }, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 204) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null, data);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.setProfileEmail = function (email, password, callback) {
|
||||
post('/api/v1/profile/email', { email: email, password: password }, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 204) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null, data);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.setProfileFallbackEmail = function (fallbackEmail, password, callback) {
|
||||
post('/api/v1/profile/fallback_email', { fallbackEmail: fallbackEmail, password: password }, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 204) return callback(new ClientError(status, data));
|
||||
|
||||
@@ -2334,15 +2486,6 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.makeUserLocal = function (userId, callback) {
|
||||
post('/api/v1/users/' + userId + '/make_local', {}, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 204) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.changePassword = function (currentPassword, newPassword, callback) {
|
||||
var data = {
|
||||
password: currentPassword,
|
||||
@@ -2474,14 +2617,19 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.refreshUserInfo = function (callback) {
|
||||
Client.prototype.refreshProfile = function (callback) {
|
||||
var that = this;
|
||||
|
||||
callback = typeof callback === 'function' ? callback : function () {};
|
||||
|
||||
this.userInfo(function (error, result) {
|
||||
this.getProfile(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (result.language !== '' && $translate.use() !== result.language) {
|
||||
console.log('Changing users language from ' + $translate.use() + ' to ', result.language);
|
||||
$translate.use(result.language);
|
||||
}
|
||||
|
||||
that.setUserInfo(result);
|
||||
callback(null);
|
||||
});
|
||||
@@ -2665,6 +2813,8 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
Client.prototype.login = function () {
|
||||
this.setToken(null);
|
||||
|
||||
localStorage.setItem('redirectToHash', window.location.hash);
|
||||
|
||||
// start oidc flow
|
||||
window.location.href = this.apiOrigin + '/openid/auth?client_id=' + ('<%= apiOrigin %>' ? TOKEN_TYPES.ID_DEVELOPMENT : TOKEN_TYPES.ID_WEBADMIN) + '&scope=openid email profile&response_type=code token&redirect_uri=' + window.location.origin + '/authcallback.html';
|
||||
};
|
||||
@@ -2998,7 +3148,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
};
|
||||
|
||||
Client.prototype.setSpamAcl = function (acl, callback) {
|
||||
post('/api/v1/mailserver/spam_acl', { whitelist: acl.whitelist, blacklist: acl.blacklist }, null, function (error, data, status) {
|
||||
post('/api/v1/mailserver/spam_acl', { allowlist: acl.allowlist, blocklist: acl.blocklist }, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
|
||||
@@ -3376,8 +3526,6 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
mountOptions: mountOptions
|
||||
};
|
||||
|
||||
console.log('---update', data)
|
||||
|
||||
post('/api/v1/volumes/' + volumeId, data, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 204) return callback(new ClientError(status, data));
|
||||
@@ -3474,11 +3622,24 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.registerCloudronWithSetupToken = function (setupToken, callback) {
|
||||
var data = {
|
||||
setupToken: setupToken
|
||||
};
|
||||
|
||||
post('/api/v1/appstore/register_cloudron_with_setup_token', data, null, function (error, data, status) {
|
||||
if (error) return callback(error);
|
||||
if (status !== 201) return callback(new ClientError(status, data));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.registerCloudron = function (email, password, totpToken, signup, callback) {
|
||||
var data = {
|
||||
email: email,
|
||||
password: password,
|
||||
signup: signup,
|
||||
signup: signup
|
||||
};
|
||||
|
||||
if (totpToken) data.totpToken = totpToken;
|
||||
@@ -3528,10 +3689,14 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
|
||||
var ACTION_DASHBOARD_DOMAIN_UPDATE = 'dashboard.domain.update';
|
||||
|
||||
var ACTION_DIRECTORY_SERVER_CONFIGURE = 'directoryserver.configure';
|
||||
|
||||
var ACTION_DOMAIN_ADD = 'domain.add';
|
||||
var ACTION_DOMAIN_UPDATE = 'domain.update';
|
||||
var ACTION_DOMAIN_REMOVE = 'domain.remove';
|
||||
|
||||
var ACTION_EXTERNAL_LDAP_CONFIGURE = 'externalldap.configure';
|
||||
|
||||
var ACTION_INSTALL_FINISH = 'cloudron.install.finish';
|
||||
|
||||
var ACTION_START = 'cloudron.start';
|
||||
@@ -3548,6 +3713,8 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
var ACTION_USER_UPDATE = 'user.update';
|
||||
var ACTION_USER_TRANSFER = 'user.transfer';
|
||||
|
||||
var ACTION_USER_DIRECTORY_PROFILE_CONFIG_UPDATE = 'userdirectory.profileconfig.update';
|
||||
|
||||
var ACTION_MAIL_LOCATION = 'mail.location';
|
||||
var ACTION_MAIL_ENABLED = 'mail.enabled';
|
||||
var ACTION_MAIL_DISABLED = 'mail.disabled';
|
||||
@@ -3579,6 +3746,11 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
return pre + (app.label || app.fqdn || app.subdomain) + ' (' + app.manifest.title + ') ';
|
||||
}
|
||||
|
||||
function eventBy() {
|
||||
if (eventLog.source && eventLog.source.username) return ' by ' + eventLog.source.username;
|
||||
return '';
|
||||
}
|
||||
|
||||
switch (eventLog.action) {
|
||||
case ACTION_ACTIVATE:
|
||||
return 'Cloudron was activated';
|
||||
@@ -3593,24 +3765,22 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
if (!data.app) return '';
|
||||
app = data.app;
|
||||
|
||||
var q = function (x) {
|
||||
return '"' + x + '"';
|
||||
};
|
||||
|
||||
if ('accessRestriction' in data) { // since it can be null
|
||||
return 'Access restriction ' + appName('of', app) + ' was changed';
|
||||
} else if ('operators' in data) {
|
||||
return 'Operators ' + appName('of', app) + ' was changed';
|
||||
} else if (data.label) {
|
||||
return 'Label ' + appName('of', app) + ' was set to ' + q(data.label);
|
||||
return `Label ${appName('of', app)} was set to ${data.label}`;
|
||||
} else if (data.tags) {
|
||||
return 'Tags ' + appName('of', app) + ' was set to ' + q(data.tags.join(','));
|
||||
return `Tags ${appName('of', app)} was set to ${data.tags.join(', ')}`;
|
||||
} else if (data.icon) {
|
||||
return 'Icon ' + appName('of', app) + ' was changed';
|
||||
} else if (data.memoryLimit) {
|
||||
return 'Memory limit ' + appName('of', app) + ' was set to ' + data.memoryLimit;
|
||||
} else if (data.cpuShares) {
|
||||
return 'Memory limit ' + appName('of', app) + ' was set to ' + prettyBinarySize(data.memoryLimit);
|
||||
} else if (data.cpuShares) { // replaced by cpuQuota in 8.0
|
||||
return 'CPU shares ' + appName('of', app) + ' was set to ' + Math.round((data.cpuShares * 100)/1024) + '%';
|
||||
} else if (data.cpuQuota) {
|
||||
return 'CPU quota ' + appName('of', app) + ' was set to ' + data.cpuQuota + '%';
|
||||
} else if (data.env) {
|
||||
return 'Env vars ' + appName('of', app) + ' was changed';
|
||||
} else if ('debugMode' in data) { // since it can be null
|
||||
@@ -3620,9 +3790,9 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
return appName('', app, 'App') + ' was taken out of repair mode';
|
||||
}
|
||||
} else if ('enableBackup' in data) {
|
||||
return 'Automatic backups ' + appName('of', app) + ' were ' + (data.enableBackup ? 'enabled' : 'disabled');
|
||||
return 'Automatic backups ' + appName('of', app) + ' was ' + (data.enableBackup ? 'enabled' : 'disabled');
|
||||
} else if ('enableAutomaticUpdate' in data) {
|
||||
return 'Automatic updates ' + appName('of', app) + ' were ' + (data.enableAutomaticUpdate ? 'enabled' : 'disabled');
|
||||
return 'Automatic updates ' + appName('of', app) + ' was ' + (data.enableAutomaticUpdate ? 'enabled' : 'disabled');
|
||||
} else if ('reverseProxyConfig' in data) {
|
||||
return 'Reverse proxy configuration ' + appName('of', app) + ' was updated';
|
||||
} else if ('upstreamUri' in data) {
|
||||
@@ -3657,11 +3827,11 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
} else {
|
||||
return 'Icon ' + appName('of', app) + ' was reset';
|
||||
}
|
||||
} else if (('mailboxName' in data) && data.mailboxName !== data.app.mailboxName) {
|
||||
} else if ('mailboxName' in data) {
|
||||
if (data.mailboxName) {
|
||||
return 'Mailbox ' + appName('of', app) + ' was set to ' + q(data.mailboxName);
|
||||
return `Mailbox ${appName('of', app)} was set to ${data.mailboxDisplayName || '' } ${data.mailboxName}@${data.mailboxDomain}`;
|
||||
} else {
|
||||
return 'Mailbox ' + appName('of', app) + ' was reset';
|
||||
return 'Mailbox ' + appName('of', app) + ' was disabled';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3670,7 +3840,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
|
||||
case ACTION_APP_INSTALL:
|
||||
if (!data.app) return '';
|
||||
return data.app.manifest.title + ' (package v' + data.app.manifest.version + ') was installed ' + appName('at', data.app);
|
||||
return data.app.manifest.title + ' (package v' + data.app.manifest.version + ') was installed ' + appName('at', data.app) + eventBy();
|
||||
|
||||
case ACTION_APP_RESTORE:
|
||||
if (!data.app) return '';
|
||||
@@ -3782,6 +3952,13 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
case ACTION_DASHBOARD_DOMAIN_UPDATE:
|
||||
return 'Dashboard domain set to ' + data.fqdn || (data.subdomain + '.' + data.domain);
|
||||
|
||||
case ACTION_DIRECTORY_SERVER_CONFIGURE:
|
||||
if (data.fromEnabled !== data.toEnabled) {
|
||||
return 'Directory server was ' + (data.toEnabled ? 'enabled' : 'disabled');
|
||||
} else {
|
||||
return 'Directory server configuration was changed';
|
||||
}
|
||||
|
||||
case ACTION_DOMAIN_ADD:
|
||||
return 'Domain ' + data.domain + ' with ' + data.provider + ' provider was added';
|
||||
|
||||
@@ -3791,6 +3968,13 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
case ACTION_DOMAIN_REMOVE:
|
||||
return 'Domain ' + data.domain + ' was removed';
|
||||
|
||||
case ACTION_EXTERNAL_LDAP_CONFIGURE:
|
||||
if (data.config.provider === 'noop') {
|
||||
return 'External Directory disabled';
|
||||
} else {
|
||||
return 'External Directory set to ' + data.config.url + ' (' + data.config.provider + ')';
|
||||
}
|
||||
|
||||
case ACTION_INSTALL_FINISH:
|
||||
return 'Cloudron version ' + data.version + ' installed';
|
||||
|
||||
@@ -3860,8 +4044,14 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
return 'Apps of ' + data.oldOwnerId + ' was transferred to ' + data.newOwnerId;
|
||||
|
||||
case ACTION_USER_LOGIN:
|
||||
app = this.getCachedAppSync(data.appId);
|
||||
return 'User ' + (data.user ? data.user.username : data.userId) + ' logged in to ' + (app ? app.fqdn : data.appId);
|
||||
if (data.mailboxId) {
|
||||
return 'User ' + (data.user ? data.user.username : data.userId) + ' logged in to mailbox ' + data.mailboxId;
|
||||
} else if (data.appId) {
|
||||
app = this.getCachedAppSync(data.appId);
|
||||
return 'User ' + (data.user ? data.user.username : data.userId) + ' logged in to ' + (app ? app.fqdn : data.appId);
|
||||
} else { // can happen with directoryserver
|
||||
return 'User ' + (data.user ? data.user.username : data.userId) + ' authenticated';
|
||||
}
|
||||
|
||||
case ACTION_USER_LOGIN_GHOST:
|
||||
return 'User ' + (data.user ? data.user.username : data.userId) + ' was impersonated';
|
||||
@@ -3869,6 +4059,9 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
case ACTION_USER_LOGOUT:
|
||||
return 'User ' + (data.user ? data.user.username : data.userId) + ' logged out';
|
||||
|
||||
case ACTION_USER_DIRECTORY_PROFILE_CONFIG_UPDATE:
|
||||
return 'User directory profile config updated. Mandatory 2FA: ' + (data.config.mandatory2FA) + ' Lock profiles: ' + (data.config.lockUserProfiles);
|
||||
|
||||
case ACTION_DYNDNS_UPDATE: {
|
||||
details = data.errorMessage ? 'Error updating DNS. ' : 'Updated DNS. ';
|
||||
if (data.fromIpv4 !== data.toIpv4) details += 'From IPv4 ' + data.fromIpv4 + ' to ' + data.toIpv4 + '. ';
|
||||
@@ -3904,8 +4097,6 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
|
||||
if (source.appId) {
|
||||
var app = this.getCachedAppSync(source.appId);
|
||||
line += ' - ' + (app ? app.fqdn : source.appId);
|
||||
} else if (source.ip) {
|
||||
line += ' - ' + source.ip;
|
||||
}
|
||||
|
||||
return line;
|
||||
|
||||
+65
-60
@@ -1,7 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular:false */
|
||||
/* global angular:false, window, document, localStorage, redirectIfNeeded */
|
||||
/* global $:false */
|
||||
/* global async */
|
||||
/* global ERROR,ISTATES,HSTATES,RSTATES,APP_TYPES,NOTIFICATION_TYPES */
|
||||
|
||||
// deal with accessToken in the query, this is passed for example on password reset and account setup upon invite
|
||||
@@ -18,7 +19,7 @@ if (search.accessToken) {
|
||||
}
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'ngFitText', 'ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'base64', 'slick', 'ui-notification', 'ui.bootstrap', 'ui.bootstrap-slider', 'ngTld', 'ui.multiselect']);
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'ngFitText', 'ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'base64', 'slick', 'ui-notification', 'ui.bootstrap', 'ui.multiselect']);
|
||||
|
||||
app.config(['NotificationProvider', function (NotificationProvider) {
|
||||
NotificationProvider.setOptions({
|
||||
@@ -118,7 +119,7 @@ app.config(['$routeProvider', function ($routeProvider) {
|
||||
}).otherwise({ redirectTo: '/'});
|
||||
}]);
|
||||
|
||||
app.filter('notificadtionTypeToColor', function () {
|
||||
app.filter('notificationTypeToColor', function () {
|
||||
return function (n) {
|
||||
switch (n.type) {
|
||||
case NOTIFICATION_TYPES.ALERT_REBOOT:
|
||||
@@ -127,6 +128,7 @@ app.filter('notificadtionTypeToColor', function () {
|
||||
case NOTIFICATION_TYPES.ALERT_CERTIFICATE_RENEWAL_FAILED:
|
||||
case NOTIFICATION_TYPES.ALERT_DISK_SPACE:
|
||||
case NOTIFICATION_TYPES.ALERT_BACKUP_CONFIG:
|
||||
case NOTIFICATION_TYPES.ALERT_BACKUP_FAILED:
|
||||
return '#ff4c4c';
|
||||
case NOTIFICATION_TYPES.ALERT_BOX_UPDATE:
|
||||
case NOTIFICATION_TYPES.ALERT_MANUAL_APP_UPDATE:
|
||||
@@ -303,6 +305,36 @@ app.filter('installationActive', function () {
|
||||
};
|
||||
});
|
||||
|
||||
// color indicator in app list
|
||||
app.filter('installationStateClass', function () {
|
||||
const ERROR_CLASS = 'status-error';
|
||||
const BUSY_CLASS = 'status-starting fa-beat-fade';
|
||||
const INACTIVE_CLASS = 'status-inactive';
|
||||
const ACTIVE_CLASS = 'status-active';
|
||||
|
||||
return function(app) {
|
||||
if (!app) return '';
|
||||
|
||||
switch (app.installationState) {
|
||||
case ISTATES.ERROR: return ERROR_CLASS;
|
||||
case ISTATES.INSTALLED: {
|
||||
if (app.debugMode) {
|
||||
return INACTIVE_CLASS;
|
||||
} else {
|
||||
if (app.runState === RSTATES.RUNNING) {
|
||||
if (!app.health) return BUSY_CLASS; // no data yet
|
||||
if (app.type === APP_TYPES.LINK || app.health === HSTATES.HEALTHY) return ACTIVE_CLASS;
|
||||
return ERROR_CLASS; // dead/exit/unhealthy
|
||||
} else {
|
||||
return INACTIVE_CLASS;
|
||||
}
|
||||
}
|
||||
}
|
||||
default: return BUSY_CLASS;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// this appears in the app grid
|
||||
app.filter('installationStateLabel', function () {
|
||||
return function(app) {
|
||||
@@ -397,7 +429,7 @@ app.filter('errorSuggestion', function () {
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('readyToUpdate', function () {
|
||||
app.filter('canUpdate', function () {
|
||||
return function (apps) {
|
||||
return apps.every(function (app) {
|
||||
return (app.installationState === ISTATES.ERROR) || (app.installationState === ISTATES.INSTALLED);
|
||||
@@ -704,7 +736,10 @@ app.controller('MainController', ['$scope', '$route', '$timeout', '$location', '
|
||||
};
|
||||
|
||||
function redirectOnMandatory2FA() {
|
||||
if (Client.getConfig().mandatory2FA && !Client.getUserInfo().twoFactorAuthenticationEnabled) {
|
||||
if (Client.getConfig().mandatory2FA) {
|
||||
if (Client.getUserInfo().twoFactorAuthenticationEnabled) return; // user already has 2fa
|
||||
if (Client.getUserInfo().source && $scope.config.external2FA) return; // 2fa is external
|
||||
|
||||
$location.path('/profile').search({ setup2fa: true });
|
||||
}
|
||||
}
|
||||
@@ -743,35 +778,12 @@ app.controller('MainController', ['$scope', '$route', '$timeout', '$location', '
|
||||
});
|
||||
}
|
||||
|
||||
function redirectIfNeeded(status) {
|
||||
if (!status.activated) {
|
||||
console.log('Not activated yet, redirecting', status);
|
||||
if (status.restore.active || status.restore.errorMessage) { // show the error message in restore page
|
||||
window.location.href = '/restore.html' + window.location.search;
|
||||
} else if (status.adminFqdn) {
|
||||
window.location.href = 'https://' + status.adminFqdn + '/setup.html' + (window.location.search);
|
||||
} else {
|
||||
window.location.href = '/setupdns.html' + window.location.search;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// support local development with localhost check
|
||||
if (window.location.hostname !== status.adminFqdn && window.location.hostname !== 'localhost' && !window.location.hostname.startsWith('192.')) {
|
||||
// user is accessing by IP or by the old admin location (pre-migration)
|
||||
window.location.href = '/setupdns.html' + window.location.search;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// this loads the very first thing when accessing via IP or domain
|
||||
function init() {
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
if (redirectIfNeeded(status)) return;
|
||||
if (redirectIfNeeded(status, 'dashboard')) return; // we got redirected...
|
||||
|
||||
// check version and force reload if needed
|
||||
if (!localStorage.version) {
|
||||
@@ -784,47 +796,40 @@ app.controller('MainController', ['$scope', '$route', '$timeout', '$location', '
|
||||
console.log('Running dashboard version ', localStorage.version);
|
||||
|
||||
// get user profile as the first thing. this populates the "scope" and affects subsequent API calls
|
||||
Client.refreshUserInfo(function (error) {
|
||||
async.series([
|
||||
Client.refreshProfile.bind(Client),
|
||||
Client.refreshConfig.bind(Client),
|
||||
Client.refreshAvailableLanguages.bind(Client),
|
||||
Client.refreshInstalledApps.bind(Client)
|
||||
], function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
Client.refreshConfig(function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
// now mark the Client to be ready
|
||||
Client.setReady();
|
||||
|
||||
Client.refreshAvailableLanguages(function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
Client.refreshInstalledApps(function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
if (Client.getUserInfo().hasBackgroundImage) {
|
||||
document.getElementById('mainContentContainer').style.backgroundImage = 'url("' + Client.getBackgroundImageUrl() + '")';
|
||||
document.getElementById('mainContentContainer').classList.add('has-background');
|
||||
}
|
||||
|
||||
// now mark the Client to be ready
|
||||
Client.setReady();
|
||||
$scope.initialized = true;
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
redirectOnMandatory2FA();
|
||||
|
||||
if (Client.getUserInfo().hasBackgroundImage) {
|
||||
document.getElementById('mainContentContainer').style.backgroundImage = 'url("' + Client.getBackgroundImageUrl() + '")';
|
||||
document.getElementById('mainContentContainer').classList.add('has-background');
|
||||
}
|
||||
$interval(refreshNotifications, 60 * 1000);
|
||||
refreshNotifications();
|
||||
|
||||
$scope.initialized = true;
|
||||
Client.getSubscription(function (error, subscription) {
|
||||
if (error && error.statusCode === 412) return; // not yet registered
|
||||
if (error && error.statusCode === 402) return; // invalid appstore token
|
||||
if (error) return console.error(error);
|
||||
|
||||
redirectOnMandatory2FA();
|
||||
$scope.subscription = subscription;
|
||||
|
||||
$interval(refreshNotifications, 60 * 1000);
|
||||
refreshNotifications();
|
||||
|
||||
Client.getSubscription(function (error, subscription) {
|
||||
if (error && error.statusCode === 412) return; // not yet registered
|
||||
if (error && error.statusCode === 402) return; // invalid appstore token
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.subscription = subscription;
|
||||
|
||||
// only track platform status if we are registered
|
||||
trackPlatformStatus();
|
||||
});
|
||||
});
|
||||
});
|
||||
// only track platform status if we are registered
|
||||
trackPlatformStatus();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -81,7 +81,8 @@ app.controller('PasswordResetController', ['$scope', '$translate', '$http', func
|
||||
identifier: $scope.passwordResetIdentifier
|
||||
};
|
||||
|
||||
function done() {
|
||||
function done(error) {
|
||||
if (error) $scope.error = error.message;
|
||||
$scope.busy = false;
|
||||
$scope.mode = 'passwordResetDone';
|
||||
}
|
||||
|
||||
+32
-36
@@ -1,17 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
/* global $, angular, tld, SECRET_PLACEHOLDER, STORAGE_PROVIDERS, BACKUP_FORMATS */
|
||||
/* global $, angular, SECRET_PLACEHOLDER, STORAGE_PROVIDERS, BACKUP_FORMATS, window, FileReader, document, redirectIfNeeded */
|
||||
/* global REGIONS_S3, REGIONS_WASABI, REGIONS_DIGITALOCEAN, REGIONS_EXOSCALE, REGIONS_SCALEWAY, REGIONS_LINODE, REGIONS_OVH, REGIONS_IONOS, REGIONS_UPCLOUD, REGIONS_VULTR, REGIONS_CONTABO */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification', 'ui.bootstrap']);
|
||||
|
||||
app.filter('zoneName', function () {
|
||||
return function (domain) {
|
||||
return tld.getDomain(domain);
|
||||
};
|
||||
});
|
||||
|
||||
app.controller('RestoreController', ['$scope', 'Client', function ($scope, Client) {
|
||||
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
@@ -51,7 +45,7 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
password: '',
|
||||
diskPath: '',
|
||||
user: '',
|
||||
seal: false,
|
||||
seal: true,
|
||||
port: 22,
|
||||
privateKey: ''
|
||||
};
|
||||
@@ -61,27 +55,25 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
$scope.mountOptions.diskPath = '/dev/disk/by-uuid/' + newValue.uuid;
|
||||
});
|
||||
|
||||
$scope.sysinfo = {
|
||||
$scope.ipv4Config = {
|
||||
provider: 'generic',
|
||||
ipv4: '',
|
||||
ip: '',
|
||||
ifname: ''
|
||||
};
|
||||
|
||||
$scope.sysinfoProvider = [
|
||||
$scope.ipv6Config = {
|
||||
provider: 'generic',
|
||||
ip: '',
|
||||
ifname: ''
|
||||
};
|
||||
|
||||
$scope.ipProviders = [
|
||||
{ name: 'Disabled', value: 'noop' },
|
||||
{ name: 'Public IP', value: 'generic' },
|
||||
{ name: 'Static IP Address', value: 'fixed' },
|
||||
{ name: 'Network Interface', value: 'network-interface' }
|
||||
];
|
||||
|
||||
$scope.prettySysinfoProviderName = function (provider) {
|
||||
switch (provider) {
|
||||
case 'generic': return 'Public IP';
|
||||
case 'fixed': return 'Static IP Address';
|
||||
case 'network-interface': return 'Network Interface';
|
||||
default: return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.s3Regions = REGIONS_S3;
|
||||
$scope.wasabiRegions = REGIONS_WASABI;
|
||||
$scope.doSpacesRegions = REGIONS_DIGITALOCEAN;
|
||||
@@ -239,16 +231,17 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
return;
|
||||
}
|
||||
|
||||
var sysinfoConfig = {
|
||||
provider: $scope.sysinfo.provider
|
||||
var data = {
|
||||
backupConfig: backupConfig,
|
||||
remotePath: $scope.remotePath.replace(/\.tar\.gz(\.enc)?$/, ''),
|
||||
version: version ? version[1] : '',
|
||||
ipv4Config: $scope.ipv4Config,
|
||||
ipv6Config: $scope.ipv6Config,
|
||||
skipDnsSetup: $scope.skipDnsSetup,
|
||||
setupToken: $scope.setupToken
|
||||
};
|
||||
if ($scope.sysinfo.provider === 'fixed') {
|
||||
sysinfoConfig.ipv4 = $scope.sysinfo.ipv4;
|
||||
} else if ($scope.sysinfo.provider === 'network-interface') {
|
||||
sysinfoConfig.ifname = $scope.sysinfo.ifname;
|
||||
}
|
||||
|
||||
Client.restore(backupConfig, $scope.remotePath.replace(/\.tar\.gz(\.enc)?$/, ''), version ? version[1] : '', sysinfoConfig, $scope.skipDnsSetup, $scope.setupToken, function (error) {
|
||||
Client.restore(data, function (error) {
|
||||
$scope.busy = false;
|
||||
|
||||
if (error) {
|
||||
@@ -303,7 +296,7 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
$scope.busy = false;
|
||||
$scope.error.generic = status.restore.errorMessage;
|
||||
} else { // restore worked, redirect to admin page
|
||||
window.location.href = '/';
|
||||
window.location.href = 'https://' + status.adminFqdn + '/';
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -362,14 +355,11 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
if (redirectIfNeeded(status, 'restore')) return; // redirected to some other view...
|
||||
|
||||
if (status.restore.active) return waitForRestore();
|
||||
|
||||
if (status.restore.errorMessage) $scope.error.generic = status.restore.errorMessage;
|
||||
|
||||
if (status.activated) {
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
if (status.restore.errorMessage) $scope.error.generic = status.restore.errorMessage; // any previous restore error
|
||||
|
||||
Client.getProvisionBlockDevices(function (error, result) {
|
||||
if (error) {
|
||||
@@ -388,7 +378,13 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
|
||||
$scope.instanceId = search.instanceId;
|
||||
$scope.setupToken = search.setupToken;
|
||||
$scope.initialized = true;
|
||||
|
||||
Client.detectIp(function (error, ip) { // this is never supposed to error
|
||||
if (!error) $scope.ipv4Config.provider = ip.ipv4 ? 'generic' : 'noop';
|
||||
if (!error) $scope.ipv6Config.provider = ip.ipv6 ? 'generic' : 'noop';
|
||||
|
||||
$scope.initialized = true;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+300
-84
@@ -1,120 +1,336 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular */
|
||||
/* global $ */
|
||||
/* global $, angular, Clipboard, ENDPOINTS_OVH, window, FileReader, document, redirectIfNeeded */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification', 'ui.bootstrap']);
|
||||
|
||||
app.controller('SetupController', ['$scope', 'Client', function ($scope, Client) {
|
||||
// Stupid angular location provider either wants html5 location mode or not, do the query parsing on my own
|
||||
app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', function ($scope, $http, $timeout, Client) {
|
||||
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
$scope.client = Client;
|
||||
$scope.view = '';
|
||||
$scope.initialized = false;
|
||||
$scope.state = null; // 'initialized', 'waitingForDnsSetup', 'waitingForBox'
|
||||
$scope.error = {};
|
||||
$scope.provider = '';
|
||||
$scope.showDNSSetup = false;
|
||||
$scope.instanceId = '';
|
||||
$scope.advancedVisible = false;
|
||||
$scope.clipboardDone = false;
|
||||
$scope.search = window.location.search;
|
||||
$scope.setupToken = '';
|
||||
$scope.taskMinutesActive = null;
|
||||
|
||||
$scope.owner = {
|
||||
error: null,
|
||||
$scope.tlsProvider = [
|
||||
{ name: 'Let\'s Encrypt Prod', value: 'letsencrypt-prod' },
|
||||
{ name: 'Let\'s Encrypt Prod - Wildcard', value: 'letsencrypt-prod-wildcard' },
|
||||
{ name: 'Let\'s Encrypt Staging', value: 'letsencrypt-staging' },
|
||||
{ name: 'Let\'s Encrypt Staging - Wildcard', value: 'letsencrypt-staging-wildcard' },
|
||||
{ name: 'Self-Signed', value: 'fallback' }, // this is not 'Custom' because we don't allow user to upload certs during setup phase
|
||||
];
|
||||
|
||||
$scope.ipv4Config = {
|
||||
provider: 'generic',
|
||||
ip: '',
|
||||
ifname: ''
|
||||
};
|
||||
|
||||
$scope.ipv6Config = {
|
||||
provider: 'generic',
|
||||
ip: '',
|
||||
ifname: ''
|
||||
};
|
||||
|
||||
$scope.ipProviders = [
|
||||
{ name: 'Disabled', value: 'noop' },
|
||||
{ name: 'Public IP', value: 'generic' },
|
||||
{ name: 'Static IP Address', value: 'fixed' },
|
||||
{ name: 'Network Interface', value: 'network-interface' }
|
||||
];
|
||||
|
||||
$scope.ovhEndpoints = ENDPOINTS_OVH;
|
||||
|
||||
$scope.needsPort80 = function (dnsProvider, tlsProvider) {
|
||||
return ((dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') &&
|
||||
(tlsProvider === 'letsencrypt-prod' || tlsProvider === 'letsencrypt-staging'));
|
||||
};
|
||||
|
||||
// If we migrate the api origin we have to poll the new location
|
||||
if (search.admin_fqdn) Client.apiOrigin = 'https://' + search.admin_fqdn;
|
||||
|
||||
// keep in sync with domains.js
|
||||
$scope.dnsProvider = [
|
||||
{ name: 'AWS Route53', value: 'route53' },
|
||||
{ name: 'Bunny', value: 'bunny' },
|
||||
{ name: 'Cloudflare', value: 'cloudflare' },
|
||||
{ name: 'deSEC', value: 'desec' },
|
||||
{ name: 'DigitalOcean', value: 'digitalocean' },
|
||||
{ name: 'DNSimple', value: 'dnsimple' },
|
||||
{ name: 'Gandi LiveDNS', value: 'gandi' },
|
||||
{ name: 'GoDaddy', value: 'godaddy' },
|
||||
{ name: 'Google Cloud DNS', value: 'gcdns' },
|
||||
{ name: 'Hetzner', value: 'hetzner' },
|
||||
{ name: 'Linode', value: 'linode' },
|
||||
{ name: 'Name.com', value: 'namecom' },
|
||||
{ name: 'Namecheap', value: 'namecheap' },
|
||||
{ name: 'Netcup', value: 'netcup' },
|
||||
{ name: 'OVH', value: 'ovh' },
|
||||
{ name: 'Porkbun', value: 'porkbun' },
|
||||
{ name: 'Vultr', value: 'vultr' },
|
||||
{ name: 'Wildcard', value: 'wildcard' },
|
||||
{ name: 'Manual (not recommended)', value: 'manual' },
|
||||
{ name: 'No-op (only for development)', value: 'noop' }
|
||||
];
|
||||
$scope.dnsCredentials = {
|
||||
busy: false,
|
||||
domain: '',
|
||||
accessKeyId: '',
|
||||
secretAccessKey: '',
|
||||
gcdnsKey: { keyFileName: '', content: '' },
|
||||
digitalOceanToken: '',
|
||||
gandiApiKey: '',
|
||||
cloudflareEmail: '',
|
||||
cloudflareToken: '',
|
||||
cloudflareTokenType: 'GlobalApiKey',
|
||||
cloudflareDefaultProxyStatus: false,
|
||||
godaddyApiKey: '',
|
||||
godaddyApiSecret: '',
|
||||
linodeToken: '',
|
||||
bunnyAccessKey: '',
|
||||
dnsimpleAccessToken: '',
|
||||
hetznerToken: '',
|
||||
vultrToken: '',
|
||||
deSecToken: '',
|
||||
nameComUsername: '',
|
||||
nameComToken: '',
|
||||
namecheapUsername: '',
|
||||
namecheapApiKey: '',
|
||||
netcupCustomerNumber: '',
|
||||
netcupApiKey: '',
|
||||
netcupApiPassword: '',
|
||||
ovhEndpoint: 'ovh-eu',
|
||||
ovhConsumerKey: '',
|
||||
ovhAppKey: '',
|
||||
ovhAppSecret: '',
|
||||
porkbunSecretapikey: '',
|
||||
porkbunApikey: '',
|
||||
|
||||
email: '',
|
||||
displayName: '',
|
||||
username: '',
|
||||
password: '',
|
||||
|
||||
submit: function () {
|
||||
$scope.owner.busy = true;
|
||||
$scope.owner.error = null;
|
||||
|
||||
var data = {
|
||||
username: $scope.owner.username,
|
||||
password: $scope.owner.password,
|
||||
email: $scope.owner.email,
|
||||
displayName: $scope.owner.displayName,
|
||||
setupToken: $scope.setupToken
|
||||
};
|
||||
|
||||
Client.createAdmin(data, function (error) {
|
||||
if (error && error.statusCode === 400) {
|
||||
$scope.owner.busy = false;
|
||||
|
||||
if (error.message === 'Invalid email') {
|
||||
$scope.owner.error = { email: error.message };
|
||||
$scope.owner.email = '';
|
||||
$scope.ownerForm.email.$setPristine();
|
||||
setTimeout(function () { $('#inputEmail').focus(); }, 200);
|
||||
} else {
|
||||
$scope.owner.error = { username: error.message };
|
||||
$scope.owner.username = '';
|
||||
$scope.ownerForm.username.$setPristine();
|
||||
setTimeout(function () { $('#inputUsername').focus(); }, 200);
|
||||
}
|
||||
return;
|
||||
} else if (error) {
|
||||
$scope.owner.busy = false;
|
||||
console.error('Internal error', error);
|
||||
$scope.owner.error = { generic: error.message };
|
||||
return;
|
||||
}
|
||||
|
||||
setView('finished');
|
||||
});
|
||||
provider: 'route53',
|
||||
zoneName: '',
|
||||
tlsConfig: {
|
||||
provider: 'letsencrypt-prod-wildcard'
|
||||
}
|
||||
};
|
||||
|
||||
function redirectIfNeeded(status) {
|
||||
if ('develop' in search || localStorage.getItem('develop')) {
|
||||
console.warn('Cloudron develop mode on. To disable run localStorage.removeItem(\'develop\')');
|
||||
localStorage.setItem('develop', true);
|
||||
return;
|
||||
$scope.setDefaultTlsProvider = function () {
|
||||
var dnsProvider = $scope.dnsCredentials.provider;
|
||||
// wildcard LE won't work without automated DNS
|
||||
if (dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') {
|
||||
$scope.dnsCredentials.tlsConfig.provider = 'letsencrypt-prod';
|
||||
} else {
|
||||
$scope.dnsCredentials.tlsConfig.provider = 'letsencrypt-prod-wildcard';
|
||||
}
|
||||
};
|
||||
|
||||
// if we are here from https://ip/setup.html ,go to https://admin/setup.html
|
||||
if (status.adminFqdn && status.adminFqdn !== window.location.hostname) {
|
||||
window.location.href = 'https://' + status.adminFqdn + '/setup.html';
|
||||
return true;
|
||||
}
|
||||
|
||||
// if we don't have a domain yet, first go to domain setup
|
||||
if (!status.adminFqdn) {
|
||||
window.location.href = '/setupdns.html';
|
||||
return true;
|
||||
}
|
||||
function readFileLocally(obj, file, fileName) {
|
||||
return function (event) {
|
||||
$scope.$apply(function () {
|
||||
obj[file] = null;
|
||||
obj[fileName] = event.target.files[0].name;
|
||||
|
||||
if (status.activated) {
|
||||
window.location.href = '/';
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (result) {
|
||||
if (!result.target || !result.target.result) return console.error('Unable to read local file');
|
||||
obj[file] = result.target.result;
|
||||
};
|
||||
reader.readAsText(event.target.files[0]);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function setView(view) {
|
||||
if (view === 'finished') {
|
||||
$scope.view = 'finished';
|
||||
} else {
|
||||
$scope.view = 'owner';
|
||||
document.getElementById('gcdnsKeyFileInput').onchange = readFileLocally($scope.dnsCredentials.gcdnsKey, 'content', 'keyFileName');
|
||||
|
||||
$scope.setDnsCredentials = function () {
|
||||
$scope.dnsCredentials.busy = true;
|
||||
$scope.error = {};
|
||||
|
||||
var provider = $scope.dnsCredentials.provider;
|
||||
|
||||
var config = {};
|
||||
|
||||
if (provider === 'route53') {
|
||||
config.accessKeyId = $scope.dnsCredentials.accessKeyId;
|
||||
config.secretAccessKey = $scope.dnsCredentials.secretAccessKey;
|
||||
} else if (provider === 'gcdns') {
|
||||
try {
|
||||
var serviceAccountKey = JSON.parse($scope.dnsCredentials.gcdnsKey.content);
|
||||
config.projectId = serviceAccountKey.project_id;
|
||||
config.credentials = {
|
||||
client_email: serviceAccountKey.client_email,
|
||||
private_key: serviceAccountKey.private_key
|
||||
};
|
||||
|
||||
if (!config.projectId || !config.credentials || !config.credentials.client_email || !config.credentials.private_key) {
|
||||
throw new Error('One or more fields are missing in the JSON');
|
||||
}
|
||||
} catch (e) {
|
||||
$scope.error.dnsCredentials = 'Cannot parse Google Service Account Key: ' + e.message;
|
||||
$scope.dnsCredentials.busy = false;
|
||||
return;
|
||||
}
|
||||
} else if (provider === 'digitalocean') {
|
||||
config.token = $scope.dnsCredentials.digitalOceanToken;
|
||||
} else if (provider === 'gandi') {
|
||||
config.token = $scope.dnsCredentials.gandiApiKey;
|
||||
} else if (provider === 'godaddy') {
|
||||
config.apiKey = $scope.dnsCredentials.godaddyApiKey;
|
||||
config.apiSecret = $scope.dnsCredentials.godaddyApiSecret;
|
||||
} else if (provider === 'cloudflare') {
|
||||
config.email = $scope.dnsCredentials.cloudflareEmail;
|
||||
config.token = $scope.dnsCredentials.cloudflareToken;
|
||||
config.tokenType = $scope.dnsCredentials.cloudflareTokenType;
|
||||
config.defaultProxyStatus = $scope.dnsCredentials.cloudflareDefaultProxyStatus;
|
||||
} else if (provider === 'linode') {
|
||||
config.token = $scope.dnsCredentials.linodeToken;
|
||||
} else if (provider === 'bunny') {
|
||||
config.accessKey = $scope.dnsCredentials.bunnyAccessKey;
|
||||
} else if (provider === 'dnsimple') {
|
||||
config.accessToken = $scope.dnsCredentials.dnsimpleAccessToken;
|
||||
} else if (provider === 'hetzner') {
|
||||
config.token = $scope.dnsCredentials.hetznerToken;
|
||||
} else if (provider === 'vultr') {
|
||||
config.token = $scope.dnsCredentials.vultrToken;
|
||||
} else if (provider === 'desec') {
|
||||
config.token = $scope.dnsCredentials.deSecToken;
|
||||
} else if (provider === 'namecom') {
|
||||
config.username = $scope.dnsCredentials.nameComUsername;
|
||||
config.token = $scope.dnsCredentials.nameComToken;
|
||||
} else if (provider === 'namecheap') {
|
||||
config.token = $scope.dnsCredentials.namecheapApiKey;
|
||||
config.username = $scope.dnsCredentials.namecheapUsername;
|
||||
} else if (provider === 'netcup') {
|
||||
config.customerNumber = $scope.dnsCredentials.netcupCustomerNumber;
|
||||
config.apiKey = $scope.dnsCredentials.netcupApiKey;
|
||||
config.apiPassword = $scope.dnsCredentials.netcupApiPassword;
|
||||
} else if (provider === 'ovh') {
|
||||
config.endpoint = $scope.dnsCredentials.ovhEndpoint;
|
||||
config.consumerKey = $scope.dnsCredentials.ovhConsumerKey;
|
||||
config.appKey = $scope.dnsCredentials.ovhAppKey;
|
||||
config.appSecret = $scope.dnsCredentials.ovhAppSecret;
|
||||
} else if (provider === 'porkbun') {
|
||||
config.apikey = $scope.dnsCredentials.porkbunApikey;
|
||||
config.secretapikey = $scope.dnsCredentials.porkbunSecretapikey;
|
||||
}
|
||||
|
||||
var tlsConfig = {
|
||||
provider: $scope.dnsCredentials.tlsConfig.provider,
|
||||
wildcard: false
|
||||
};
|
||||
if ($scope.dnsCredentials.tlsConfig.provider.indexOf('-wildcard') !== -1) {
|
||||
tlsConfig.provider = tlsConfig.provider.replace('-wildcard', '');
|
||||
tlsConfig.wildcard = true;
|
||||
}
|
||||
|
||||
var data = {
|
||||
domainConfig: {
|
||||
domain: $scope.dnsCredentials.domain,
|
||||
zoneName: $scope.dnsCredentials.zoneName,
|
||||
provider: provider,
|
||||
config: config,
|
||||
tlsConfig: tlsConfig
|
||||
},
|
||||
ipv4Config: $scope.ipv4Config,
|
||||
ipv6Config: $scope.ipv6Config,
|
||||
providerToken: $scope.instanceId,
|
||||
setupToken: $scope.setupToken
|
||||
};
|
||||
|
||||
Client.setup(data, function (error) {
|
||||
if (error) {
|
||||
$scope.dnsCredentials.busy = false;
|
||||
if (error.statusCode === 422) {
|
||||
if (provider === 'ami') {
|
||||
$scope.error.ami = error.message;
|
||||
} else {
|
||||
$scope.error.setup = error.message;
|
||||
}
|
||||
} else {
|
||||
$scope.error.dnsCredentials = error.message;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
waitForDnsSetup();
|
||||
});
|
||||
};
|
||||
|
||||
function waitForDnsSetup() {
|
||||
$scope.state = 'waitingForDnsSetup';
|
||||
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
if (!error && !status.setup.active) {
|
||||
if (!status.adminFqdn || status.setup.errorMessage) { // setup reset or errored. start over
|
||||
$scope.error.setup = status.setup.errorMessage;
|
||||
$scope.state = 'initialized';
|
||||
$scope.dnsCredentials.busy = false;
|
||||
} else { // proceed to activation
|
||||
window.location.href = 'https://' + status.adminFqdn + '/activation.html' + (window.location.search);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!error) {
|
||||
$scope.message = status.setup.message;
|
||||
$scope.taskMinutesActive = (new Date() - new Date(status.setup.startTime)) / 60000;
|
||||
}
|
||||
|
||||
setTimeout(waitForDnsSetup, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
$scope.state = 'waitingForBox';
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
if (redirectIfNeeded(status)) return;
|
||||
setView(search.view);
|
||||
if (redirectIfNeeded(status, 'setup')) return; // redirected to some other view...
|
||||
|
||||
if (status.setup.active) return waitForDnsSetup();
|
||||
|
||||
$scope.error.setup = status.setup.errorMessage; // show any previous error
|
||||
|
||||
if (status.provider === 'digitalocean' || status.provider === 'digitalocean-mp') {
|
||||
$scope.dnsCredentials.provider = 'digitalocean';
|
||||
} else if (status.provider === 'linode' || status.provider === 'linode-oneclick' || status.provider === 'linode-stackscript') {
|
||||
$scope.dnsCredentials.provider = 'linode';
|
||||
} else if (status.provider === 'vultr' || status.provider === 'vultr-mp') {
|
||||
$scope.dnsCredentials.provider = 'vultr';
|
||||
} else if (status.provider === 'gce') {
|
||||
$scope.dnsCredentials.provider = 'gcdns';
|
||||
} else if (status.provider === 'ami') {
|
||||
// aws marketplace made a policy change that they one cannot provide route53 IAM credentials
|
||||
$scope.dnsCredentials.provider = 'wildcard';
|
||||
}
|
||||
|
||||
$scope.instanceId = search.instanceId;
|
||||
$scope.setupToken = search.setupToken;
|
||||
$scope.initialized = true;
|
||||
$scope.provider = status.provider;
|
||||
|
||||
// Ensure we have a good autofocus
|
||||
setTimeout(function () {
|
||||
$(document).find("[autofocus]:first").focus();
|
||||
}, 250);
|
||||
Client.detectIp(function (error, ip) { // this is never supposed to error
|
||||
if (!error) $scope.ipv4Config.provider = ip.ipv4 ? 'generic' : 'noop';
|
||||
if (!error) $scope.ipv6Config.provider = ip.ipv6 ? 'generic' : 'noop';
|
||||
|
||||
$scope.state = 'initialized';
|
||||
|
||||
setTimeout(function () { $("[autofocus]:first").focus(); }, 100);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var clipboard = new Clipboard('.clipboard');
|
||||
clipboard.on('success', function () {
|
||||
$scope.$apply(function () { $scope.clipboardDone = true; });
|
||||
$timeout(function () { $scope.clipboardDone = false; }, 5000);
|
||||
});
|
||||
|
||||
init();
|
||||
}]);
|
||||
|
||||
@@ -71,6 +71,7 @@ app.controller('SetupAccountController', ['$scope', '$translate', '$http', funct
|
||||
$scope.error = null;
|
||||
$scope.view = 'setup';
|
||||
$scope.branding = null;
|
||||
$scope.dashboardUrl = '';
|
||||
|
||||
$scope.profileLocked = !!search.profileLocked;
|
||||
$scope.existingUsername = !!search.username;
|
||||
@@ -122,8 +123,10 @@ app.controller('SetupAccountController', ['$scope', '$translate', '$http', funct
|
||||
$http.post(API_ORIGIN + '/api/v1/auth/setup_account', data).success(function (data, status) {
|
||||
if (status !== 201) return error(data, status);
|
||||
|
||||
// set token to autologin
|
||||
localStorage.token = data.accessToken;
|
||||
// set token to autologin on first oidc flow
|
||||
localStorage.cloudronFirstTimeToken = data.accessToken;
|
||||
|
||||
$scope.dashboardUrl = '/openid/auth?client_id=cid-webadmin&scope=openid email profile&response_type=code token&redirect_uri=' + window.location.origin + '/authcallback.html';
|
||||
|
||||
$scope.view = 'done';
|
||||
}).error(error);
|
||||
|
||||
@@ -1,337 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global $, tld, angular, Clipboard */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification', 'ui.bootstrap']);
|
||||
|
||||
app.filter('zoneName', function () {
|
||||
return function (domain) {
|
||||
return tld.getDomain(domain);
|
||||
};
|
||||
});
|
||||
|
||||
app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', function ($scope, $http, $timeout, Client) {
|
||||
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
$scope.state = null; // 'initialized', 'waitingForDnsSetup', 'waitingForBox'
|
||||
$scope.error = {};
|
||||
$scope.provider = '';
|
||||
$scope.showDNSSetup = false;
|
||||
$scope.instanceId = '';
|
||||
$scope.isDomain = false;
|
||||
$scope.isSubdomain = false;
|
||||
$scope.advancedVisible = false;
|
||||
$scope.clipboardDone = false;
|
||||
$scope.search = window.location.search;
|
||||
$scope.setupToken = '';
|
||||
|
||||
$scope.tlsProvider = [
|
||||
{ name: 'Let\'s Encrypt Prod', value: 'letsencrypt-prod' },
|
||||
{ name: 'Let\'s Encrypt Prod - Wildcard', value: 'letsencrypt-prod-wildcard' },
|
||||
{ name: 'Let\'s Encrypt Staging', value: 'letsencrypt-staging' },
|
||||
{ name: 'Let\'s Encrypt Staging - Wildcard', value: 'letsencrypt-staging-wildcard' },
|
||||
{ name: 'Self-Signed', value: 'fallback' }, // this is not 'Custom' because we don't allow user to upload certs during setup phase
|
||||
];
|
||||
|
||||
$scope.sysinfo = {
|
||||
provider: 'generic',
|
||||
ipv4: '',
|
||||
ifname: ''
|
||||
};
|
||||
|
||||
$scope.sysinfoProvider = [
|
||||
{ name: 'Public IP', value: 'generic' },
|
||||
{ name: 'Static IP Address', value: 'fixed' },
|
||||
{ name: 'Network Interface', value: 'network-interface' }
|
||||
];
|
||||
|
||||
$scope.prettySysinfoProviderName = function (provider) {
|
||||
switch (provider) {
|
||||
case 'generic': return 'Public IP';
|
||||
case 'fixed': return 'Static IP Address';
|
||||
case 'network-interface': return 'Network Interface';
|
||||
default: return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.needsPort80 = function (dnsProvider, tlsProvider) {
|
||||
return ((dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') &&
|
||||
(tlsProvider === 'letsencrypt-prod' || tlsProvider === 'letsencrypt-staging'));
|
||||
};
|
||||
|
||||
// If we migrate the api origin we have to poll the new location
|
||||
if (search.admin_fqdn) Client.apiOrigin = 'https://' + search.admin_fqdn;
|
||||
|
||||
$scope.$watch('dnsCredentials.domain', function (newVal) {
|
||||
if (!newVal) {
|
||||
$scope.isDomain = false;
|
||||
$scope.isSubdomain = false;
|
||||
} else if (!tld.getDomain(newVal) || newVal[newVal.length-1] === '.') {
|
||||
$scope.isDomain = false;
|
||||
$scope.isSubdomain = false;
|
||||
} else {
|
||||
$scope.isDomain = true;
|
||||
$scope.isSubdomain = tld.getDomain(newVal) !== newVal;
|
||||
}
|
||||
});
|
||||
|
||||
// keep in sync with domains.js
|
||||
$scope.dnsProvider = [
|
||||
{ name: 'AWS Route53', value: 'route53' },
|
||||
{ name: 'Bunny', value: 'bunny' },
|
||||
{ name: 'Cloudflare', value: 'cloudflare' },
|
||||
{ name: 'DigitalOcean', value: 'digitalocean' },
|
||||
{ name: 'Gandi LiveDNS', value: 'gandi' },
|
||||
{ name: 'GoDaddy', value: 'godaddy' },
|
||||
{ name: 'Google Cloud DNS', value: 'gcdns' },
|
||||
{ name: 'Hetzner', value: 'hetzner' },
|
||||
{ name: 'Linode', value: 'linode' },
|
||||
{ name: 'Name.com', value: 'namecom' },
|
||||
{ name: 'Namecheap', value: 'namecheap' },
|
||||
{ name: 'Netcup', value: 'netcup' },
|
||||
{ name: 'Porkbun', value: 'porkbun' },
|
||||
{ name: 'Vultr', value: 'vultr' },
|
||||
{ name: 'Wildcard', value: 'wildcard' },
|
||||
{ name: 'Manual (not recommended)', value: 'manual' },
|
||||
{ name: 'No-op (only for development)', value: 'noop' }
|
||||
];
|
||||
$scope.dnsCredentials = {
|
||||
busy: false,
|
||||
domain: '',
|
||||
accessKeyId: '',
|
||||
secretAccessKey: '',
|
||||
gcdnsKey: { keyFileName: '', content: '' },
|
||||
digitalOceanToken: '',
|
||||
gandiApiKey: '',
|
||||
cloudflareEmail: '',
|
||||
cloudflareToken: '',
|
||||
cloudflareTokenType: 'GlobalApiKey',
|
||||
cloudflareDefaultProxyStatus: false,
|
||||
godaddyApiKey: '',
|
||||
godaddyApiSecret: '',
|
||||
linodeToken: '',
|
||||
bunnyAccessKey: '',
|
||||
hetznerToken: '',
|
||||
vultrToken: '',
|
||||
nameComUsername: '',
|
||||
nameComToken: '',
|
||||
namecheapUsername: '',
|
||||
namecheapApiKey: '',
|
||||
netcupCustomerNumber: '',
|
||||
netcupApiKey: '',
|
||||
netcupApiPassword: '',
|
||||
porkbunSecretapikey: '',
|
||||
porkbunApikey: '',
|
||||
|
||||
provider: 'route53',
|
||||
zoneName: '',
|
||||
tlsConfig: {
|
||||
provider: 'letsencrypt-prod-wildcard'
|
||||
}
|
||||
};
|
||||
|
||||
$scope.setDefaultTlsProvider = function () {
|
||||
var dnsProvider = $scope.dnsCredentials.provider;
|
||||
// wildcard LE won't work without automated DNS
|
||||
if (dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') {
|
||||
$scope.dnsCredentials.tlsConfig.provider = 'letsencrypt-prod';
|
||||
} else {
|
||||
$scope.dnsCredentials.tlsConfig.provider = 'letsencrypt-prod-wildcard';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function readFileLocally(obj, file, fileName) {
|
||||
return function (event) {
|
||||
$scope.$apply(function () {
|
||||
obj[file] = null;
|
||||
obj[fileName] = event.target.files[0].name;
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (result) {
|
||||
if (!result.target || !result.target.result) return console.error('Unable to read local file');
|
||||
obj[file] = result.target.result;
|
||||
};
|
||||
reader.readAsText(event.target.files[0]);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById('gcdnsKeyFileInput').onchange = readFileLocally($scope.dnsCredentials.gcdnsKey, 'content', 'keyFileName');
|
||||
|
||||
$scope.setDnsCredentials = function () {
|
||||
$scope.dnsCredentials.busy = true;
|
||||
$scope.error = {};
|
||||
|
||||
var provider = $scope.dnsCredentials.provider;
|
||||
|
||||
var config = {};
|
||||
|
||||
if (provider === 'route53') {
|
||||
config.accessKeyId = $scope.dnsCredentials.accessKeyId;
|
||||
config.secretAccessKey = $scope.dnsCredentials.secretAccessKey;
|
||||
} else if (provider === 'gcdns') {
|
||||
try {
|
||||
var serviceAccountKey = JSON.parse($scope.dnsCredentials.gcdnsKey.content);
|
||||
config.projectId = serviceAccountKey.project_id;
|
||||
config.credentials = {
|
||||
client_email: serviceAccountKey.client_email,
|
||||
private_key: serviceAccountKey.private_key
|
||||
};
|
||||
|
||||
if (!config.projectId || !config.credentials || !config.credentials.client_email || !config.credentials.private_key) {
|
||||
throw new Error('One or more fields are missing in the JSON');
|
||||
}
|
||||
} catch (e) {
|
||||
$scope.error.dnsCredentials = 'Cannot parse Google Service Account Key: ' + e.message;
|
||||
$scope.dnsCredentials.busy = false;
|
||||
return;
|
||||
}
|
||||
} else if (provider === 'digitalocean') {
|
||||
config.token = $scope.dnsCredentials.digitalOceanToken;
|
||||
} else if (provider === 'gandi') {
|
||||
config.token = $scope.dnsCredentials.gandiApiKey;
|
||||
} else if (provider === 'godaddy') {
|
||||
config.apiKey = $scope.dnsCredentials.godaddyApiKey;
|
||||
config.apiSecret = $scope.dnsCredentials.godaddyApiSecret;
|
||||
} else if (provider === 'cloudflare') {
|
||||
config.email = $scope.dnsCredentials.cloudflareEmail;
|
||||
config.token = $scope.dnsCredentials.cloudflareToken;
|
||||
config.tokenType = $scope.dnsCredentials.cloudflareTokenType;
|
||||
config.defaultProxyStatus = $scope.dnsCredentials.cloudflareDefaultProxyStatus;
|
||||
} else if (provider === 'linode') {
|
||||
config.token = $scope.dnsCredentials.linodeToken;
|
||||
} else if (provider === 'bunny') {
|
||||
config.token = $scope.dnsCredentials.bunnyAccessKey;
|
||||
} else if (provider === 'hetzner') {
|
||||
config.token = $scope.dnsCredentials.hetznerToken;
|
||||
} else if (provider === 'vultr') {
|
||||
config.token = $scope.dnsCredentials.vultrToken;
|
||||
} else if (provider === 'namecom') {
|
||||
config.username = $scope.dnsCredentials.nameComUsername;
|
||||
config.token = $scope.dnsCredentials.nameComToken;
|
||||
} else if (provider === 'namecheap') {
|
||||
config.token = $scope.dnsCredentials.namecheapApiKey;
|
||||
config.username = $scope.dnsCredentials.namecheapUsername;
|
||||
} else if (provider === 'netcup') {
|
||||
config.customerNumber = $scope.dnsCredentials.netcupCustomerNumber;
|
||||
config.apiKey = $scope.dnsCredentials.netcupApiKey;
|
||||
config.apiPassword = $scope.dnsCredentials.netcupApiPassword;
|
||||
} else if (provider === 'porkbun') {
|
||||
config.apikey = $scope.dnsCredentials.porkbunApikey;
|
||||
config.secretapikey = $scope.dnsCredentials.porkbunSecretapikey;
|
||||
}
|
||||
|
||||
var tlsConfig = {
|
||||
provider: $scope.dnsCredentials.tlsConfig.provider,
|
||||
wildcard: false
|
||||
};
|
||||
if ($scope.dnsCredentials.tlsConfig.provider.indexOf('-wildcard') !== -1) {
|
||||
tlsConfig.provider = tlsConfig.provider.replace('-wildcard', '');
|
||||
tlsConfig.wildcard = true;
|
||||
}
|
||||
|
||||
var sysinfoConfig = {
|
||||
provider: $scope.sysinfo.provider
|
||||
};
|
||||
if ($scope.sysinfo.provider === 'fixed') {
|
||||
sysinfoConfig.ipv4 = $scope.sysinfo.ipv4;
|
||||
} else if ($scope.sysinfo.provider === 'network-interface') {
|
||||
sysinfoConfig.ifname = $scope.sysinfo.ifname;
|
||||
}
|
||||
|
||||
var data = {
|
||||
domainConfig: {
|
||||
domain: $scope.dnsCredentials.domain,
|
||||
zoneName: $scope.dnsCredentials.zoneName,
|
||||
provider: provider,
|
||||
config: config,
|
||||
tlsConfig: tlsConfig
|
||||
},
|
||||
ipv4Config: sysinfoConfig,
|
||||
providerToken: $scope.instanceId,
|
||||
setupToken: $scope.setupToken
|
||||
};
|
||||
|
||||
Client.setup(data, function (error) {
|
||||
if (error) {
|
||||
$scope.dnsCredentials.busy = false;
|
||||
if (error.statusCode === 422) {
|
||||
if (provider === 'ami') {
|
||||
$scope.error.ami = error.message;
|
||||
} else {
|
||||
$scope.error.setup = error.message;
|
||||
}
|
||||
} else {
|
||||
$scope.error.dnsCredentials = error.message;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
waitForDnsSetup();
|
||||
});
|
||||
};
|
||||
|
||||
function waitForDnsSetup() {
|
||||
$scope.state = 'waitingForDnsSetup';
|
||||
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
if (!error && !status.setup.active) {
|
||||
if (!status.adminFqdn || status.setup.errorMessage) { // setup reset or errored. start over
|
||||
$scope.error.setup = status.setup.errorMessage;
|
||||
$scope.state = 'initialized';
|
||||
$scope.dnsCredentials.busy = false;
|
||||
} else { // proceed to activation
|
||||
window.location.href = 'https://' + status.adminFqdn + '/setup.html' + (window.location.search);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.message = status.setup.message;
|
||||
|
||||
setTimeout(waitForDnsSetup, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
if (error) {
|
||||
// During domain migration, the box code restarts and can result in getStatus() failing temporarily
|
||||
console.error(error);
|
||||
$scope.state = 'waitingForBox';
|
||||
return $timeout(initialize, 3000);
|
||||
}
|
||||
|
||||
// domain is currently like a lock flag
|
||||
if (status.adminFqdn) return waitForDnsSetup();
|
||||
|
||||
if (status.provider === 'digitalocean' || status.provider === 'digitalocean-mp') {
|
||||
$scope.dnsCredentials.provider = 'digitalocean';
|
||||
} else if (status.provider === 'linode' || status.provider === 'linode-oneclick' || status.provider === 'linode-stackscript') {
|
||||
$scope.dnsCredentials.provider = 'linode';
|
||||
} else if (status.provider === 'vultr' || status.provider === 'vultr-mp') {
|
||||
$scope.dnsCredentials.provider = 'vultr';
|
||||
} else if (status.provider === 'gce') {
|
||||
$scope.dnsCredentials.provider = 'gcdns';
|
||||
} else if (status.provider === 'ami') {
|
||||
$scope.dnsCredentials.provider = 'route53';
|
||||
}
|
||||
|
||||
$scope.instanceId = search.instanceId;
|
||||
$scope.setupToken = search.setupToken;
|
||||
$scope.provider = status.provider;
|
||||
$scope.state = 'initialized';
|
||||
|
||||
setTimeout(function () { $("[autofocus]:first").focus(); }, 100);
|
||||
});
|
||||
}
|
||||
|
||||
var clipboard = new Clipboard('.clipboard');
|
||||
clipboard.on('success', function () {
|
||||
$scope.$apply(function () { $scope.clipboardDone = true; });
|
||||
$timeout(function () { $scope.clipboardDone = false; }, 5000);
|
||||
});
|
||||
|
||||
initialize();
|
||||
}]);
|
||||
@@ -23,7 +23,7 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-family: "Roboto","Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||
font-family: "Noto Sans", Helvetica, Arial, sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.846;
|
||||
}
|
||||
@@ -51,9 +51,19 @@
|
||||
|
||||
<script type="text/javascript">
|
||||
window.addEventListener('load', (event) => {
|
||||
document.getElementById('message').innerHTML =
|
||||
'You are seeing this page because the DNS record of <b>' + window.location.hostname + '</b> is set to this server\'s IP'
|
||||
// https://stackoverflow.com/questions/37437890/check-if-url-has-domain-name-and-not-an-ip
|
||||
const containsLetter = /[a-zA-z]/.test(window.location.hostname); // ignore technicality that IP can contain letters ! http://192.168.0x1.0x1 or http://0xc0.0xa8.1.1
|
||||
const isIPv6 = location.hostname.startsWith('[') && location.hostname.endsWith(']');
|
||||
|
||||
let message;
|
||||
if (!containsLetter || isIPv6) { // ipv4 or ipv6
|
||||
message = 'You cannot view Cloudron dashboard by IP address. Instead, navigate to the domain you configured during setup i.e <b>https://my.domain.example</b> .'
|
||||
+ '<br>If you do not remember your domain, SSH into your server and run <code>cloudron-support --owner-login</code> .'
|
||||
} else { // hostname
|
||||
message = 'You are seeing this page because the DNS record of <b>' + window.location.hostname + '</b> is set to this server\'s IP'
|
||||
+ ' but Cloudron has no app configured for this domain.';
|
||||
}
|
||||
document.getElementById('message').innerHTML = message;
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css?<%= revision %>">
|
||||
|
||||
<!-- Fontawesome -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.css?<%= revision %>"/>
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.min.css?<%= revision %>"/>
|
||||
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js?<%= revision %>"></script>
|
||||
@@ -89,7 +89,8 @@
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" style="margin-top: -84px" src="<%= apiOrigin %>/api/v1/cloudron/avatar"/>
|
||||
<br/>
|
||||
<h2>{{ 'passwordReset.emailSent.title' | tr }}</h2>
|
||||
<h2 ng-hide="error">{{ 'passwordReset.emailSent.title' | tr }}</h2>
|
||||
<h4 ng-show="error" class="has-error">{{ error }}</h4>
|
||||
<br/>
|
||||
<a href="/" class="btn btn-primary">{{ 'passwordReset.backToLoginAction' | tr }}</a>
|
||||
</div>
|
||||
|
||||
+337
-323
@@ -1,54 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<title>Cloudron Restore</title>
|
||||
<meta name="description" content="Cloudron Restore">
|
||||
<title>Cloudron Restore</title>
|
||||
<meta name="description" content="Cloudron Restore">
|
||||
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css">
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css">
|
||||
|
||||
<!-- Fontawesome -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.css?<%= revision %>"/>
|
||||
<!-- Fontawesome -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.min.css?<%= revision %>"/>
|
||||
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js"></script>
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js"></script>
|
||||
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js"></script>
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js"></script>
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js"></script>
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js"></script>
|
||||
|
||||
<!-- Angularjs scripts -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/autofill-event.js"></script>
|
||||
<!-- Angularjs scripts -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/autofill-event.js"></script>
|
||||
|
||||
<!-- Angular directives for tldjs -->
|
||||
<script type="text/javascript" src="/3rdparty/js/tld.js"></script>
|
||||
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js"></script>
|
||||
|
||||
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js"></script>
|
||||
<!-- Angular translate https://angular-translate.github.io/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-loader-static-files.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-cookie.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-local.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Angular translate https://angular-translate.github.io/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-loader-static-files.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-cookie.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-local.min.js?<%= revision %>"></script>
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Setup Application -->
|
||||
<script type="text/javascript" src="/js/restore.js"></script>
|
||||
<!-- Setup Application -->
|
||||
<script type="text/javascript" src="/js/restore.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
@@ -56,300 +53,317 @@
|
||||
|
||||
<a class="offline-banner animateMe" ng-show="client.offline" ng-cloak href="https://docs.cloudron.io/troubleshooting/" target="_blank"><i class="fa fa-circle-notch fa-spin"></i> Cloudron is offline. Reconnecting...</a>
|
||||
|
||||
<div class="main-container ng-cloak text-center" ng-show="busy">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<i class="fa fa-circle-notch fa-spin fa-5x"></i><br/>
|
||||
<h3>{{ message }} ...</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main-container ng-cloak text-center" ng-show="busy">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<i class="fa fa-circle-notch fa-spin fa-5x"></i><br/>
|
||||
<h3>{{ message }} ...</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-container ng-cloak" ng-show="initialized && !busy">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="card" style="max-width: none; padding: 20px;">
|
||||
<form name="configureBackupForm" role="form" novalidate ng-submit="restore()" autocomplete="off">
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1 text-center">
|
||||
<h2>Cloudron Restore</h2>
|
||||
<p>Provide the backup to restore from</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main-container ng-cloak" ng-show="initialized && !busy">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="card" style="max-width: none; padding: 20px;">
|
||||
<form name="configureBackupForm" role="form" novalidate ng-submit="restore()" autocomplete="off">
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1 text-center">
|
||||
<h2>Cloudron Restore</h2>
|
||||
<p>Provide the backup to restore from</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-bottom: 20px">
|
||||
<div class="col-md-8 col-md-offset-2 text-center">
|
||||
<input type="file" id="backupConfigFileInput" style="display:none"/>
|
||||
<button type="button" class="btn btn-default" onclick="getElementById('backupConfigFileInput').click();">Upload Backup Config</button>
|
||||
</div>
|
||||
<br/>
|
||||
</div>
|
||||
<div class="row" style="margin-bottom: 20px">
|
||||
<div class="col-md-8 col-md-offset-2 text-center">
|
||||
<input type="file" id="backupConfigFileInput" style="display:none"/>
|
||||
<button type="button" class="btn btn-default" onclick="getElementById('backupConfigFileInput').click();">Upload Backup Config</button>
|
||||
</div>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<p class="has-error text-center" ng-show="error">{{ error.generic }}</p>
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<p class="has-error text-center" ng-show="error">{{ error.generic }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="storageProviderProvider">Storage provider <sup><a ng-href="https://docs.cloudron.io/backups/#storage-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" id="storageProviderProvider" ng-model="provider" ng-options="a.value as a.name for a in storageProviders" ng-change=clearForm()></select>
|
||||
</div>
|
||||
|
||||
<!-- mountpoint -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.mountPoint }" ng-show="provider === 'mountpoint'">
|
||||
<label class="control-label" for="inputConfigureMountPoint">Mountpoint</label>
|
||||
<input type="text" class="form-control" ng-model="mountPoint" id="inputConfigureMountPoint" name="mountPoint" ng-disabled="busy" placeholder="Folder where filesystem is mounted" ng-required="provider === 'mountpoint'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'cifs' || provider === 'nfs' || provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupHost">Server IP or Hostname</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.host" id="configureBackupHost" name="host" ng-disabled="busy" placeholder="Server IP or hostname" ng-required="provider === 'cifs' || provider === 'nfs'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="checkbox" ng-show="provider === 'cifs'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mountOptions.seal">Use seal encryption. Requires at least SMB v3</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'cifs' || provider === 'nfs' || provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupRemoteDir">Remote Directory</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.remoteDir" id="configureBackupRemoteDir" name="remoteDir" ng-disabled="busy" placeholder="/share" ng-required="provider === 'cifs' || provider === 'nfs'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="form-group" ng-show="provider === 'cifs'">
|
||||
<label class="control-label" for="configureBackupUsername">Username ({{ provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.username" id="configureBackupUsername" name="cifsUsername" ng-disabled="busy">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="form-group" ng-show="provider === 'cifs'">
|
||||
<label class="control-label" for="configureBackupPassword">Password ({{ provider }})</label>
|
||||
<input type="password" class="form-control" ng-model="mountOptions.password" id="configureBackupPassword" name="cifsPassword" ng-disabled="busy" password-reveal>
|
||||
</div>
|
||||
|
||||
<!-- EXT4/XFS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.diskPath }" ng-show="provider === 'ext4' || provider === 'xfs'">
|
||||
<label class="control-label" for="inputConfigureDiskPath">Disk Path</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.diskPath" id="inputConfigureDiskPath" name="diskPath" ng-disabled="busy" placeholder="Directory for backups" ng-required="provider === 'ext4' || provider === 'xfs'">
|
||||
</div>
|
||||
|
||||
<!-- Disk -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.diskPath }" ng-show="provider === 'disk'">
|
||||
<label class="control-label">Device</label>
|
||||
<select class="form-control" ng-model="disk" ng-options="item as item.label for item in blockDevices track by item.path" ng-required="provider === 'disk'"></select>
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupPort">SSH Port</label>
|
||||
<input type="number" class="form-control" ng-model="mountOptions.port" id="configureBackupPort" name="port" ng-disabled="busy">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupUser">SSH User</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.user" id="configureBackupUser" name="user" ng-disabled="busy">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupPrivateKey">SSH Private Key</label>
|
||||
<textarea class="form-control" ng-model="mountOptions.privateKey" id="configureBackupPrivateKey" name="privateKey" ng-disabled="busy"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Filesystem -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.backupFolder }" ng-show="provider === 'filesystem'">
|
||||
<label class="control-label" for="inputConfigureBackupFolder">Local backup directory</label>
|
||||
<input type="text" class="form-control" ng-model="backupFolder" id="inputConfigureBackupFolder" name="backupFolder" ng-disabled="busy" placeholder="Directory for backups" ng-required="provider === 'filesystem'">
|
||||
</div>
|
||||
|
||||
<!-- S3/Minio/SOS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.endpoint }" ng-show="provider === 'minio' || provider === 'upcloud-objectstorage' || provider === 'backblaze-b2' || provider === 'cloudflare-r2' || provider === 's3-v4-compat' || provider === 'idrive-e2'">
|
||||
<label class="control-label" for="inputConfigureBackupEndpoint">Endpoint</label>
|
||||
<input type="text" class="form-control" ng-model="endpoint" id="inputConfigureBackupEndpoint" name="endpoint" ng-disabled="busy" placeholder="URL" ng-required="provider === 'minio' || provider === 'upcloud-objectstorage' || provider === 'backblaze-b2' || provider === 'cloudflare-r2' || provider === 's3-v4-compat' || provider === 'idrive-e2'">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="provider === 'minio' || provider === 's3-v4-compat'" >
|
||||
<label>
|
||||
<input type="checkbox" ng-model="acceptSelfSignedCerts" id="inputConfigureBackupSelfSigned">
|
||||
Accept Self-signed certificate
|
||||
</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.bucket }" ng-show="s3like(provider) || provider === 'gcs'">
|
||||
<label class="control-label" for="inputConfigureBackupBucket">Bucket name</label>
|
||||
<input type="text" class="form-control" ng-model="bucket" id="inputConfigureBackupBucket" name="bucket" ng-disabled="busy" ng-required="s3like(provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.prefix }" ng-show="provider !== 'filesystem' && provider !== 'noop'">
|
||||
<label class="control-label" for="inputConfigureBackupPrefix">Prefix</label>
|
||||
<input type="text" class="form-control" ng-model="prefix" id="inputConfigureBackupPrefix" name="prefix" ng-disabled="busy" placeholder="Prefix for backup file names">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 's3'">
|
||||
<label class="control-label" for="inputConfigureBackupS3Region">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupS3Region" ng-model="region" ng-options="a.value as a.name for a in s3Regions" ng-disabled="busy" ng-required="provider === 's3'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 's3-v4-compat'">
|
||||
<label class="control-label" for="inputConfigureBackupS3V4CompatRegion">Region</label>
|
||||
<input class="form-control" type="text" name="region" id="inputConfigureBackupS3V4CompatRegion" ng-model="region" ng-disabled="busy" placeholder="Leave empty to use us-east-1 as default"></input>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'digitalocean-spaces'">
|
||||
<label class="control-label" for="inputConfigureBackupDORegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupDORegion" ng-model="endpoint" ng-options="a.value as a.name for a in doSpacesRegions" ng-disabled="busy" ng-required="provider === 'digitalocean-spaces'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'exoscale-sos'">
|
||||
<label class="control-label" for="inputConfigureBackupExoscaleRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupExoscaleRegion" ng-model="endpoint" ng-options="a.value as a.name for a in exoscaleSosRegions" ng-disabled="busy" ng-required="provider === 'exoscale-sos'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'wasabi'">
|
||||
<label class="control-label" for="inputConfigureBackupWasabiRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupWasabiRegion" ng-model="endpoint" ng-options="a.value as a.name for a in wasabiRegions" ng-disabled="busy" ng-required="provider === 'wasabi'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'scaleway-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupScalewayRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupScalewayRegion" ng-model="endpoint" ng-options="a.value as a.name for a in scalewayRegions" ng-disabled="busy" ng-required="provider === 'scaleway-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'linode-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupLinodeRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupLinodeRegion" ng-model="endpoint" ng-options="a.value as a.name for a in linodeRegions" ng-disabled="busy" ng-required="provider === 'linode-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'ovh-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupOvhRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupOvhRegion" ng-model="endpoint" ng-options="a.value as a.name for a in ovhRegions" ng-disabled="busy" ng-required="provider === 'ovh-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'ionos-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupIonosRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupIonosRegion" ng-model="endpoint" ng-options="a.value as a.name for a in ionosRegions" ng-disabled="busy" ng-required="provider === 'ionos-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'vultr-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupVultrRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupVultrRegion" ng-model="endpoint" ng-options="a.value as a.name for a in vultrRegions" ng-disabled="busy" ng-required="provider === 'vultr-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'contabo-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupContaboRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupContaboRegion" ng-model="endpoint" ng-options="a.value as a.name for a in contaboRegions" ng-disabled="busy" ng-required="provider === 'contabo-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.accessKeyId }" ng-show="s3like(provider)">
|
||||
<label class="control-label" for="inputConfigureBackupAccessKeyId">Access key id</label>
|
||||
<input type="text" class="form-control" ng-model="accessKeyId" id="inputConfigureBackupAccessKeyId" name="accessKeyId" ng-disabled="busy" ng-required="s3like(provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.secretAccessKey }" ng-show="s3like(provider)">
|
||||
<label class="control-label" for="inputConfigureBackupSecretAccessKey">Secret access key</label>
|
||||
<input type="text" class="form-control" ng-model="secretAccessKey" id="inputConfigureBackupSecretAccessKey" name="secretAccessKey" ng-disabled="busy" ng-required="s3like(provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.gcsKeyInput }" ng-show="provider === 'gcs'">
|
||||
<label class="control-label" for="gcsKeyInput">Service Account Key</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input type="file" id="gcsKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="gcsKey.keyFileName" id="gcsKeyInput" name="cert" onclick="getElementById('gcsKeyFileInput').click();" style="cursor: pointer;" ng-disabled="busy" ng-required="provider === 'gcs'">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('gcsKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="storageFormat">Storage Format</label>
|
||||
<select class="form-control" id="storageFormat" ng-change="key = ''" ng-model="format" ng-options="a.value as a.name for a in formats"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.remotePath }">
|
||||
<label class="control-label" for="inputConfigureRemotePath">Backup Path</label>
|
||||
|
||||
<input type="text" class="form-control" ng-model="remotePath" name="inputConfigureBackupId" placeholder="Backup Path" required ng-disabled="busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.key }">
|
||||
<label class="control-label" for="inputConfigureBackupPassword">Encryption password <span ng-hide="encrypted">(optional)</span></label>
|
||||
<input type="text" class="form-control" ng-model="password" id="inputConfigureBackupPassword" name="prefix" ng-disabled="busy" placeholder="Passphrase used to encrypt the backups" ng-required="encrypted">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="format === 'rsync' && password.length !== 0">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="encryptedFilenames">Decrypt Filenames</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="skipDnsSetup"><b>Dry run</b></sup>
|
||||
</label>
|
||||
<br/>
|
||||
<small>When enabled, apps are restored but the DNS records are not updated to point to this server. To access the dashboard, this browser's host must have an entry in <code>/etc/hosts</code> for the dashboard domain to this server's IP.
|
||||
See the <a href="https://docs.cloudron.io/backups/#dry-run" target="_blank">docs</a> for more information.</small>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="configureBackupForm.$invalid"/>
|
||||
|
||||
<div uib-collapse="!advancedVisible">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">IP Configuration <sup><a ng-href="https://docs.cloudron.io/networking/#ip-configuration" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="sysinfo.provider" ng-options="a.value as a.name for a in sysinfoProvider"></select>
|
||||
</div>
|
||||
|
||||
<!-- Fixed -->
|
||||
<div class="form-group" ng-show="sysinfo.provider === 'fixed'" ng-class="{ 'has-error': error.ipv4 }">
|
||||
<label class="control-label">IP Address</label>
|
||||
<input type="text" class="form-control" ng-model="sysinfo.ipv4" name="ipv4" ng-disabled="sysinfo.busy" ng-required="sysinfo.provider === 'fixed'">
|
||||
<p class="has-error" ng-show="error.ipv4">{{ error.ipv4 }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Network Interface -->
|
||||
<div class="form-group" ng-show="sysinfo.provider === 'network-interface'" ng-class="{ 'has-error': error.ifname }">
|
||||
<label class="control-label">Interface Name</label>
|
||||
<input type="text" class="form-control" ng-model="sysinfo.ifname" name="ifname" ng-disabled="sysinfo.busy" ng-required="sysinfo.provider === 'network-interface'">
|
||||
<p class="has-error" ng-show="error.ifname">{{ error.ifname }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<a href="" ng-click="advancedVisible = true" ng-hide="advancedVisible">Advanced settings...</a>
|
||||
<a href="" ng-click="advancedVisible = false" ng-show="advancedVisible">Hide Advanced settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<button type="submit" class="btn btn-primary" ng-disabled="configureBackupForm.$invalid"/><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> Restore</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="storageProviderProvider">Storage provider <sup><a ng-href="https://docs.cloudron.io/backups/#storage-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" id="storageProviderProvider" ng-model="provider" ng-options="a.value as a.name for a in storageProviders" ng-change=clearForm()></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted">©2022 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted"><a href="https://forum.cloudron.io" target="_blank">Forum <i class="fa fa-comments"></i></a></span>
|
||||
</footer>
|
||||
<!-- mountpoint -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.mountPoint }" ng-show="provider === 'mountpoint'">
|
||||
<label class="control-label" for="inputConfigureMountPoint">Mountpoint</label>
|
||||
<input type="text" class="form-control" ng-model="mountPoint" id="inputConfigureMountPoint" name="mountPoint" ng-disabled="busy" placeholder="Folder where filesystem is mounted" ng-required="provider === 'mountpoint'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'cifs' || provider === 'nfs' || provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupHost">Server IP or Hostname</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.host" id="configureBackupHost" name="host" ng-disabled="busy" placeholder="Server IP or hostname" ng-required="provider === 'cifs' || provider === 'nfs'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="checkbox" ng-show="provider === 'cifs'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mountOptions.seal">Use seal encryption. Requires at least SMB v3</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'cifs' || provider === 'nfs' || provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupRemoteDir">Remote Directory</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.remoteDir" id="configureBackupRemoteDir" name="remoteDir" ng-disabled="busy" placeholder="/share" ng-required="provider === 'cifs' || provider === 'nfs'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="form-group" ng-show="provider === 'cifs'">
|
||||
<label class="control-label" for="configureBackupUsername">Username ({{ provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.username" id="configureBackupUsername" name="cifsUsername" ng-disabled="busy">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="form-group" ng-show="provider === 'cifs'">
|
||||
<label class="control-label" for="configureBackupPassword">Password ({{ provider }})</label>
|
||||
<input type="password" class="form-control" ng-model="mountOptions.password" id="configureBackupPassword" name="cifsPassword" ng-disabled="busy" password-reveal>
|
||||
</div>
|
||||
|
||||
<!-- EXT4/XFS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.diskPath }" ng-show="provider === 'ext4' || provider === 'xfs'">
|
||||
<label class="control-label" for="inputConfigureDiskPath">Disk Path</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.diskPath" id="inputConfigureDiskPath" name="diskPath" ng-disabled="busy" placeholder="Directory for backups" ng-required="provider === 'ext4' || provider === 'xfs'">
|
||||
</div>
|
||||
|
||||
<!-- Disk -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.diskPath }" ng-show="provider === 'disk'">
|
||||
<label class="control-label">Device</label>
|
||||
<select class="form-control" ng-model="disk" ng-options="item as item.label for item in blockDevices track by item.path" ng-required="provider === 'disk'"></select>
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupPort">SSH Port</label>
|
||||
<input type="number" class="form-control" ng-model="mountOptions.port" id="configureBackupPort" name="port" ng-disabled="busy">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupUser">SSH User</label>
|
||||
<input type="text" class="form-control" ng-model="mountOptions.user" id="configureBackupUser" name="user" ng-disabled="busy">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupPrivateKey">SSH Private Key</label>
|
||||
<textarea class="form-control" ng-model="mountOptions.privateKey" id="configureBackupPrivateKey" name="privateKey" ng-disabled="busy"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Filesystem -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.backupFolder }" ng-show="provider === 'filesystem'">
|
||||
<label class="control-label" for="inputConfigureBackupFolder">Local backup directory</label>
|
||||
<input type="text" class="form-control" ng-model="backupFolder" id="inputConfigureBackupFolder" name="backupFolder" ng-disabled="busy" placeholder="Directory for backups" ng-required="provider === 'filesystem'">
|
||||
</div>
|
||||
|
||||
<!-- S3/Minio/SOS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.endpoint }" ng-show="provider === 'minio' || provider === 'upcloud-objectstorage' || provider === 'backblaze-b2' || provider === 'cloudflare-r2' || provider === 's3-v4-compat' || provider === 'idrive-e2'">
|
||||
<label class="control-label" for="inputConfigureBackupEndpoint">Endpoint</label>
|
||||
<input type="text" class="form-control" ng-model="endpoint" id="inputConfigureBackupEndpoint" name="endpoint" ng-disabled="busy" placeholder="URL" ng-required="provider === 'minio' || provider === 'upcloud-objectstorage' || provider === 'backblaze-b2' || provider === 'cloudflare-r2' || provider === 's3-v4-compat' || provider === 'idrive-e2'">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="provider === 'minio' || provider === 's3-v4-compat'" >
|
||||
<label>
|
||||
<input type="checkbox" ng-model="acceptSelfSignedCerts" id="inputConfigureBackupSelfSigned">
|
||||
Accept Self-signed certificate
|
||||
</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.bucket }" ng-show="s3like(provider) || provider === 'gcs'">
|
||||
<label class="control-label" for="inputConfigureBackupBucket">Bucket name</label>
|
||||
<input type="text" class="form-control" ng-model="bucket" id="inputConfigureBackupBucket" name="bucket" ng-disabled="busy" ng-required="s3like(provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.prefix }" ng-show="provider !== 'filesystem' && provider !== 'noop'">
|
||||
<label class="control-label" for="inputConfigureBackupPrefix">Prefix</label>
|
||||
<input type="text" class="form-control" ng-model="prefix" id="inputConfigureBackupPrefix" name="prefix" ng-disabled="busy" placeholder="Prefix for backup file names">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 's3'">
|
||||
<label class="control-label" for="inputConfigureBackupS3Region">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupS3Region" ng-model="region" ng-options="a.value as a.name for a in s3Regions" ng-disabled="busy" ng-required="provider === 's3'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 's3-v4-compat'">
|
||||
<label class="control-label" for="inputConfigureBackupS3V4CompatRegion">Region</label>
|
||||
<input class="form-control" type="text" name="region" id="inputConfigureBackupS3V4CompatRegion" ng-model="region" ng-disabled="busy" placeholder="Leave empty to use us-east-1 as default"></input>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'digitalocean-spaces'">
|
||||
<label class="control-label" for="inputConfigureBackupDORegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupDORegion" ng-model="endpoint" ng-options="a.value as a.name for a in doSpacesRegions" ng-disabled="busy" ng-required="provider === 'digitalocean-spaces'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'exoscale-sos'">
|
||||
<label class="control-label" for="inputConfigureBackupExoscaleRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupExoscaleRegion" ng-model="endpoint" ng-options="a.value as a.name for a in exoscaleSosRegions" ng-disabled="busy" ng-required="provider === 'exoscale-sos'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'wasabi'">
|
||||
<label class="control-label" for="inputConfigureBackupWasabiRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupWasabiRegion" ng-model="endpoint" ng-options="a.value as a.name for a in wasabiRegions" ng-disabled="busy" ng-required="provider === 'wasabi'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'scaleway-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupScalewayRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupScalewayRegion" ng-model="endpoint" ng-options="a.value as a.name for a in scalewayRegions" ng-disabled="busy" ng-required="provider === 'scaleway-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'linode-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupLinodeRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupLinodeRegion" ng-model="endpoint" ng-options="a.value as a.name for a in linodeRegions" ng-disabled="busy" ng-required="provider === 'linode-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'ovh-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupOvhRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupOvhRegion" ng-model="endpoint" ng-options="a.value as a.name for a in ovhRegions" ng-disabled="busy" ng-required="provider === 'ovh-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'ionos-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupIonosRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupIonosRegion" ng-model="endpoint" ng-options="a.value as a.name for a in ionosRegions" ng-disabled="busy" ng-required="provider === 'ionos-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'vultr-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupVultrRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupVultrRegion" ng-model="endpoint" ng-options="a.value as a.name for a in vultrRegions" ng-disabled="busy" ng-required="provider === 'vultr-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'contabo-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupContaboRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupContaboRegion" ng-model="endpoint" ng-options="a.value as a.name for a in contaboRegions" ng-disabled="busy" ng-required="provider === 'contabo-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.accessKeyId }" ng-show="s3like(provider)">
|
||||
<label class="control-label" for="inputConfigureBackupAccessKeyId">Access key id</label>
|
||||
<input type="text" class="form-control" ng-model="accessKeyId" id="inputConfigureBackupAccessKeyId" name="accessKeyId" ng-disabled="busy" ng-required="s3like(provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.secretAccessKey }" ng-show="s3like(provider)">
|
||||
<label class="control-label" for="inputConfigureBackupSecretAccessKey">Secret access key</label>
|
||||
<input type="text" class="form-control" ng-model="secretAccessKey" id="inputConfigureBackupSecretAccessKey" name="secretAccessKey" ng-disabled="busy" ng-required="s3like(provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.gcsKeyInput }" ng-show="provider === 'gcs'">
|
||||
<label class="control-label" for="gcsKeyInput">Service Account Key</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input type="file" id="gcsKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="gcsKey.keyFileName" id="gcsKeyInput" name="cert" onclick="getElementById('gcsKeyFileInput').click();" style="cursor: pointer;" ng-disabled="busy" ng-required="provider === 'gcs'">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('gcsKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="storageFormat">Storage Format</label>
|
||||
<select class="form-control" id="storageFormat" ng-change="key = ''" ng-model="format" ng-options="a.value as a.name for a in formats"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.remotePath }">
|
||||
<label class="control-label" for="inputConfigureRemotePath">Backup Path<sup><a ng-href="https://docs.cloudron.io/backups/#restore-cloudron" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
|
||||
<input type="text" class="form-control" ng-model="remotePath" name="inputConfigureBackupId" placeholder="e.g. 2024-02-20-130007-637/box_v7.4.3.tar.gz" required ng-disabled="busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.key }">
|
||||
<label class="control-label" for="inputConfigureBackupPassword">Encryption password <span ng-hide="encrypted">(optional)</span></label>
|
||||
<input type="text" class="form-control" ng-model="password" id="inputConfigureBackupPassword" name="prefix" ng-disabled="busy" placeholder="Passphrase used to encrypt the backups" ng-required="encrypted">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="format === 'rsync' && password.length !== 0">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="encryptedFilenames">Decrypt Filenames</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="skipDnsSetup"><b>Dry run</b></sup>
|
||||
</label>
|
||||
<br/>
|
||||
<small>When enabled, apps are restored but the DNS records are not updated to point to this server. To access the dashboard, this browser's host must have an entry in <code>/etc/hosts</code> for the dashboard domain to this server's IP.
|
||||
See the <a href="https://docs.cloudron.io/backups/#dry-run" target="_blank">docs</a> for more information.</small>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="configureBackupForm.$invalid"/>
|
||||
|
||||
<div uib-collapse="!advancedVisible">
|
||||
<!-- IPv4 provider -->
|
||||
<div class="form-group">
|
||||
<label class="control-label">IPv4 Configuration <sup><a ng-href="https://docs.cloudron.io/networking/#ip-configuration" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="ipv4Config.provider" ng-options="a.value as a.name for a in ipProviders"></select>
|
||||
</div>
|
||||
|
||||
<!-- IPv4 Fixed -->
|
||||
<div class="form-group" ng-show="ipv4Config.provider === 'fixed'">
|
||||
<label class="control-label">IPv4 Address</label>
|
||||
<input type="text" class="form-control" ng-model="ipv4Config.ip" name="ipv4" ng-required="ipv4Config.provider === 'fixed'">
|
||||
</div>
|
||||
|
||||
<!-- IPv4 Network Interface -->
|
||||
<div class="form-group" ng-show="ipv4Config.provider === 'network-interface'">
|
||||
<label class="control-label">IPv4 Interface Name</label>
|
||||
<input type="text" class="form-control" ng-model="ipv4Config.ifname" name="ifname4" ng-required="ipv4Config.provider === 'network-interface'">
|
||||
</div>
|
||||
|
||||
|
||||
<!-- IPv6 provider -->
|
||||
<div class="form-group">
|
||||
<label class="control-label">IPv6 Configuration <sup><a ng-href="https://docs.cloudron.io/networking/#ip-configuration" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="ipv6Config.provider" ng-options="a.value as a.name for a in ipProviders"></select>
|
||||
</div>
|
||||
|
||||
<!-- IPv6 Fixed -->
|
||||
<div class="form-group" ng-show="ipv6Config.provider === 'fixed'">
|
||||
<label class="control-label">IPv6 Address</label>
|
||||
<input type="text" class="form-control" ng-model="ipv6Config.ip" name="ipv6" ng-required="ipv6Config.provider === 'fixed'">
|
||||
</div>
|
||||
|
||||
<!-- IPv6 Network Interface -->
|
||||
<div class="form-group" ng-show="ipv6Config.provider === 'network-interface'">
|
||||
<label class="control-label">IPv6 Interface Name</label>
|
||||
<input type="text" class="form-control" ng-model="ipv6Config.ifname" name="ifname6" ng-required="ipv6Config.provider === 'network-interface'">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<a href="" ng-click="advancedVisible = true" ng-hide="advancedVisible">Advanced settings...</a>
|
||||
<a href="" ng-click="advancedVisible = false" ng-show="advancedVisible">Hide Advanced settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<button type="submit" class="btn btn-primary" ng-disabled="configureBackupForm.$invalid"/><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> Restore</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted">©2022 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted"><a href="https://forum.cloudron.io" target="_blank">Forum <i class="fa fa-comments"></i></a></span>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+346
-108
@@ -1,152 +1,390 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<title>Cloudron Setup</title>
|
||||
<meta name="description" content="Cloudron Setup">
|
||||
<title>Cloudron Domain Setup</title>
|
||||
<meta name="description" content="Cloudron Domain Setup">
|
||||
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css">
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css">
|
||||
|
||||
<!-- Fontawesome -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.css?<%= revision %>"/>
|
||||
<!-- Fontawesome -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.min.css?<%= revision %>"/>
|
||||
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js"></script>
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js"></script>
|
||||
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js"></script>
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js"></script>
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js"></script>
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js"></script>
|
||||
|
||||
<!-- Angularjs scripts -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/autofill-event.js"></script>
|
||||
<!-- Angularjs scripts -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/autofill-event.js"></script>
|
||||
|
||||
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/clipboard.min.js"></script>
|
||||
|
||||
<!-- Angular translate https://angular-translate.github.io/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-loader-static-files.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-cookie.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-local.min.js?<%= revision %>"></script>
|
||||
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js"></script>
|
||||
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></script>
|
||||
<!-- Angular translate https://angular-translate.github.io/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-loader-static-files.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-cookie.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-local.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Setup Application -->
|
||||
<script type="text/javascript" src="/js/setup.js"></script>
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Setup Application -->
|
||||
<script type="text/javascript" src="/js/setup.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="setup" ng-app="Application" ng-controller="SetupController">
|
||||
<body class="setup" ng-app="Application" ng-controller="SetupDNSController">
|
||||
|
||||
<a class="offline-banner animateMe" ng-show="client.offline" ng-cloak href="https://docs.cloudron.io/troubleshooting/" target="_blank"><i class="fa fa-circle-notch fa-spin"></i> Cloudron is offline. Reconnecting...</a>
|
||||
|
||||
<div class="main-container" ng-show="initialized">
|
||||
<div class="row" ng-show="view === 'owner'">
|
||||
<div class="main-container ng-cloak text-center" ng-show="state === 'waitingForDnsSetup'">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3 text-center">
|
||||
<i class="fa fa-circle-notch fa-spin fa-5x"></i><br/>
|
||||
<h3>{{ message }} ...</h3>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<p>
|
||||
Please wait while Cloudron is setting up the dashboard.<br/>
|
||||
You can follow the logs on the server at <code class="clipboard hand" data-clipboard-text="/home/yellowtent/platformdata/logs/box.log" uib-tooltip="{{ clipboardDone ? 'Copied' : 'Click to copy' }}" tooltip-placement="right">/home/yellowtent/platformdata/logs/box.log</code>
|
||||
</p>
|
||||
<br/>
|
||||
<br/>
|
||||
<p ng-show="taskMinutesActive >= 4">
|
||||
If setup appears stuck, it can be restarted by running <code class="clipboard hand" data-clipboard-text="systemctl restart box" uib-tooltip="{{ clipboardDone ? 'Copied' : 'Click to copy' }}" tooltip-placement="right">systemctl restart box</code> and reloading this page.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-container ng-cloak" ng-show="state === 'initialized'">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="card" style="max-width: none; padding: 20px;">
|
||||
<form role="form" name="ownerForm" ng-submit="owner.submit()" novalidate>
|
||||
<form name="dnsCredentialsForm" role="form" novalidate ng-submit="setDnsCredentials()" autocomplete="off">
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<h1>Welcome to Cloudron</h1>
|
||||
<h3>Set up Admin Account</h3>
|
||||
<p class="has-error text-center" ng-show="owner.error.generic">{{ owner.error.generic }}</p>
|
||||
<div class="col-md-10 col-md-offset-1 text-center">
|
||||
<h1>Domain Setup</h1>
|
||||
<p class="has-error text-center" ng-show="error.setup">{{ error.setup }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<div class="form-group" ng-class="{ 'has-error': ownerForm.displayName.$dirty && ownerForm.displayName.$invalid }">
|
||||
<label class="control-label" for="inputDisplayName">Full Name</label>
|
||||
<input type="text" class="form-control" ng-model="owner.displayName" id="inputDisplayName" name="displayName" placeholder="Full Name" required autocomplete="off" ng-disabled="owner.busy" autofocus>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (ownerForm.email.$dirty && ownerForm.email.$invalid) || (!ownerForm.email.$dirty && owner.error.email) }">
|
||||
<label class="control-label" for="inputEmail">Email <sup><a ng-href="https://docs.cloudron.io/installation/#admin-account" class="help" target="_blank" tabIndex="-1"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<input type="email" class="form-control" ng-model="owner.email" id="inputEmail" name="email" placeholder="Email" required autocomplete="off" ng-disabled="owner.busy">
|
||||
<small>A valid email is required for Let's Encrypt certificates. This email is local to your Cloudron. </small>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (ownerForm.username.$dirty && ownerForm.username.$invalid) || (!ownerForm.username.$dirty && owner.error.username) }">
|
||||
<label class="control-label" for="inputUsername">Username</label>
|
||||
<input type="text" class="form-control" ng-model="owner.username" id="inputUsername" name="username" placeholder="Username" ng-maxlength="512" ng-minlength="1" required autocomplete="off" ng-disabled="owner.busy">
|
||||
<small>{{ owner.error.username }}</small>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 0;" ng-class="{ 'has-error': ownerForm.password.$dirty && ownerForm.password.$invalid }">
|
||||
<label class="control-label" for="inputPassword">Password</label>
|
||||
<input type="password" class="form-control" ng-model="owner.password" id="inputPassword" name="password" placeholder="Password" ng-pattern="/^.{8,}$/" required autocomplete="off" ng-disabled="owner.busy" password-reveal>
|
||||
<small><span ng-show="ownerForm.password.$dirty && ownerForm.password.$invalid">Password must be at least 8 characters</span> </small>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="form-group" style="margin-bottom: 0;" ng-class="{ 'has-error': dnsCredentialsForm.domain.$dirty && dnsCredentialsForm.domain.$invalid }">
|
||||
<label class="control-label">Domain <sup><a ng-href="https://docs.cloudron.io/installation/#domain-setup" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.domain" name="domain" placeholder="example.com" required autofocus ng-disabled="dnsCredentials.busy">
|
||||
<div class="text-danger" ng-show="dnsCredentials.domain.indexOf('my.') === 0 && dnsCredentials.domain.length > 3">Are you sure about this domain? The dashboard will be at <b>my.{{ dnsCredentials.domain }}</b></div>
|
||||
<p style="margin-top: 5px; font-size: 13px;">
|
||||
Apps will be installed on subdomains of this domain. The dashboard will be available on the <b>my</b> subdomain. You can add more domains later.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<h3 class="text-center">Domain Configuration <sup><a ng-href="https://docs.cloudron.io/domains/#dns-providers" class="help" target="_blank" tabindex="-1"><i class="fa fa-question-circle"></i></a></sup> </h3>
|
||||
<p class="has-error text-center" ng-show="error.dnsCredentials">{{ error.dnsCredentials }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">DNS Provider</label>
|
||||
<select class="form-control" ng-model="dnsCredentials.provider" ng-options="a.value as a.name for a in dnsProvider" ng-disabled="dnsCredentials.busy" ng-change="setDefaultTlsProvider()"></select>
|
||||
</div>
|
||||
|
||||
<!-- Route53 -->
|
||||
<div ng-if="provider === 'ami'" ng-show="dnsCredentials.provider === 'route53'">
|
||||
<b class="has-error">This feature is disabled in AWS Marketplace AMI. <a href="https://docs.aws.amazon.com/marketplace/latest/userguide/product-and-ami-policies.html" target="_blank">AWS Marketplace Policy</a> disallows
|
||||
AMIs from requesting IAM credentials from users to access Route53 hosted domains. Please use the Wildcard or Manual provider instead.</b>
|
||||
</div>
|
||||
|
||||
<div ng-if="provider !== 'ami'" class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.accessKeyId.$dirty && dnsCredentialsForm.accessKeyId.$invalid }" ng-show="dnsCredentials.provider === 'route53'">
|
||||
<label class="control-label">Access Key Id</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.accessKeyId" name="accessKeyId" placeholder="Access Key Id" ng-minlength="16" ng-maxlength="32" ng-required="dnsCredentials.provider === 'route53'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div ng-if="provider !== 'ami'" class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.secretAccessKey.$dirty && dnsCredentialsForm.secretAccessKey.$invalid }" ng-show="dnsCredentials.provider === 'route53'">
|
||||
<label class="control-label">Secret Access Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.secretAccessKey" name="secretAccessKey" placeholder="Secret Access Key" ng-required="dnsCredentials.provider === 'route53'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Google Cloud DNS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="dnsCredentials.provider === 'gcdns'">
|
||||
<label class="control-label">Service Account Key</label>
|
||||
<div class="input-group">
|
||||
<input type="file" id="gcdnsKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="dnsCredentials.gcdnsKey.keyFileName" id="gcdnsKeyInput" name="cert" onclick="getElementById('gcdnsKeyFileInput').click();" style="cursor: pointer;" ng-required="dnsCredentials.provider === 'gcdns'" ng-disabled="dnsCredentials.busy">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('gcdnsKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DigitalOcean -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.digitalOceanToken.$dirty && dnsCredentialsForm.digitalOceanToken.$invalid }" ng-show="dnsCredentials.provider === 'digitalocean'">
|
||||
<label class="control-label">DigitalOcean Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.digitalOceanToken" name="digitalOceanToken" placeholder="API Token" ng-required="dnsCredentials.provider === 'digitalocean'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Gandi -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.gandiApiKey.$dirty && dnsCredentialsForm.gandiApiKey.$invalid }" ng-show="dnsCredentials.provider === 'gandi'">
|
||||
<label class="control-label">Gandi API Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.gandiApiKey" name="gandiApiKey" placeholder="API Key" ng-required="dnsCredentials.provider === 'gandi'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- GoDaddy -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.godaddyApiKey.$dirty && dnsCredentialsForm.godaddyApiKey.$invalid }" ng-show="dnsCredentials.provider === 'godaddy'">
|
||||
<label class="control-label">API Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.godaddyApiKey" name="godaddyApiKey" placeholder="API Key" ng-minlength="1" ng-required="dnsCredentials.provider === 'godaddy'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.godaddyApiSecret.$dirty && dnsCredentialsForm.godaddyApiSecret.$invalid }" ng-show="dnsCredentials.provider === 'godaddy'">
|
||||
<label class="control-label">API Secret</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.godaddyApiSecret" name="godaddyApiSecret" placeholder="API Secret" ng-required="dnsCredentials.provider === 'godaddy'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Netcup -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.netcupCustomerNumber.$dirty && dnsCredentialsForm.netcupCustomerNumber.$invalid }" ng-show="dnsCredentials.provider === 'netcup'">
|
||||
<label class="control-label">Customer Number</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.netcupCustomerNumber" name="netcupCustomerNumber" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'netcup'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.netcupApiKey.$dirty && dnsCredentialsForm.netcupApiKey.$invalid }" ng-show="dnsCredentials.provider === 'netcup'">
|
||||
<label class="control-label">API Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.netcupApiKey" name="netcupApiKey" ng-disabled="dnsCredentials.busy" ng-minlength="1" ng-required="dnsCredentials.provider === 'netcup'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.netcupApiPassword.$dirty && dnsCredentialsForm.netcupApiPassword.$invalid }" ng-show="dnsCredentials.provider === 'netcup'">
|
||||
<label class="control-label">API Password</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.netcupApiPassword" name="netcupApiPassword" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'netcup'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Cloudflare -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.cloudflareToken.$dirty && dnsCredentialsForm.cloudflareToken.$invalid }" ng-show="dnsCredentials.provider === 'cloudflare'">
|
||||
<label class="control-label">Token Type</label>
|
||||
<select class="form-control" ng-model="dnsCredentials.cloudflareTokenType">
|
||||
<option value="GlobalApiKey">Global API Key</option>
|
||||
<option value="ApiToken">API Token</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.cloudflareToken.$dirty && dnsCredentialsForm.cloudflareToken.$invalid }" ng-show="dnsCredentials.provider === 'cloudflare'">
|
||||
<label class="control-label" ng-show="dnsCredentials.cloudflareTokenType === 'GlobalApiKey'">Global API Key</label>
|
||||
<label class="control-label" ng-show="dnsCredentials.cloudflareTokenType === 'ApiToken'">API Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.cloudflareToken" name="cloudflareToken" placeholder="API Key/Token" ng-required="dnsCredentials.provider === 'cloudflare'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.cloudflareEmail.$dirty && dnsCredentialsForm.cloudflareEmail.$invalid }" ng-show="dnsCredentials.provider === 'cloudflare' && dnsCredentials.cloudflareTokenType === 'GlobalApiKey'">
|
||||
<label class="control-label">Cloudflare Email</label>
|
||||
<input type="email" class="form-control" ng-model="dnsCredentials.cloudflareEmail" name="cloudflareEmail" placeholder="Cloudflare Account Email" ng-required="dnsCredentials.provider === 'cloudflare' && dnsCredentials.cloudflareTokenType === 'GlobalApiKey'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="dnsCredentials.provider === 'cloudflare'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="dnsCredentials.cloudflareDefaultProxyStatus"> Enable proxying for new DNS records
|
||||
<sup><a ng-href="https://docs.cloudron.io/domains/#cloudflare-dns" class="help" target="_blank" tabIndex="-1"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Name.com -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.nameComUsername.$dirty && dnsCredentialsForm.nameComUsername.$invalid }" ng-show="dnsCredentials.provider === 'namecom'">
|
||||
<label class="control-label">Name.com Username</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.nameComUsername" name="nameComUsername" placeholder="Name.com Username" ng-required="dnsCredentials.provider === 'namecom'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.nameComToken.$dirty && dnsCredentialsForm.nameComToken.$invalid }" ng-show="dnsCredentials.provider === 'namecom'">
|
||||
<label class="control-label">API Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.nameComToken" name="nameComToken" placeholder="Name.com API Token" ng-required="dnsCredentials.provider === 'namecom'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Namecheap -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.namecheapUsername.$dirty && dnsCredentialsForm.namecheapUsername.$invalid }" ng-show="dnsCredentials.provider === 'namecheap'">
|
||||
<label class="control-label">Namecheap Username</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.namecheapUsername" name="namecheapUsername" placeholder="Namecheap Username" ng-required="dnsCredentials.provider === 'namecheap'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.namecheapApiKey.$dirty && dnsCredentialsForm.namecheapApiKey.$invalid }" ng-show="dnsCredentials.provider === 'namecheap'">
|
||||
<label class="control-label">API Key</label>
|
||||
<p class="small text-info" ng-show="dnsCredentials.provider === 'namecheap'"><b>The server IP needs to be whitelisted for this API Key.</b></p>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.namecheapApiKey" name="namecheapApiKey" placeholder="Namecheap API Key" ng-required="dnsCredentials.provider === 'namecheap'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Linode -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'linode'">
|
||||
<label class="control-label">API Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.linodeToken" name="linodeToken" ng-required="dnsCredentials.provider === 'linode'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
|
||||
<!-- Bunny -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'bunny'">
|
||||
<label class="control-label">Access Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.bunnyAccessKey" name="bunnyAccessKey" ng-required="dnsCredentials.provider === 'bunny'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
|
||||
<!-- dnsimple -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'dnsimple'">
|
||||
<label class="control-label">Access Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.dnsimpleAccessToken" name="dnsimpleAccessToken" ng-required="dnsCredentials.provider === 'dnsimple'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
|
||||
<!-- OVH -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'ovh'">
|
||||
<label class="control-label" for="inputConfigureOvhEndpoint">Endpoint</label>
|
||||
<select class="form-control" name="endpoint" id="inputConfigureOvhEndpoint" ng-model="dnsCredentials.ovhEndpoint" ng-options="a.value as a.name for a in ovhEndpoints" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'ovh'"></select>
|
||||
</p>
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'ovh'">
|
||||
<label class="control-label">Consumer Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.ovhConsumerKey" name="ovhConsumerKey" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'ovh'">
|
||||
</p>
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'ovh'">
|
||||
<label class="control-label">Application Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.ovhAppKey" name="ovhAppKey" ng-disabled="dnsCredentials.busy" ng-minlength="1" ng-required="dnsCredentials.provider === 'ovh'">
|
||||
</p>
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'ovh'">
|
||||
<label class="control-label">Application Secret</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.ovhAppSecret" name="ovhAppSecret" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'ovh'">
|
||||
</p>
|
||||
|
||||
<!-- Porkbun -->
|
||||
<p class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.porkbunApikey.$dirty && dnsCredentialsForm.porkbunApikey.$invalid }" ng-show="dnsCredentials.provider === 'porkbun'">
|
||||
<label class="control-label">API Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.porkbunApikey" name="porkbunApikey" placeholder="API Key" ng-minlength="1" ng-required="dnsCredentials.provider === 'porkbun'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
<p class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.porkbunSecretapikey.$dirty && dnsCredentialsForm.porkbunSecretapikey.$invalid }" ng-show="dnsCredentials.provider === 'porkbun'">
|
||||
<label class="control-label">API Secret</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.porkbunSecretapikey" name="porkbunSecretapikey" placeholder="API Secret" ng-required="dnsCredentials.provider === 'porkbun'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
|
||||
<!-- Hetzner -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'hetzner'">
|
||||
<label class="control-label">API Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.hetznerToken" name="hetznerToken" ng-required="dnsCredentials.provider === 'hetzner'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
|
||||
<!-- Vultr -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'vultr'">
|
||||
<label class="control-label">API Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.vultrToken" name="vultrToken" ng-required="dnsCredentials.provider === 'vultr'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
|
||||
<!-- deSEC -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'desec'">
|
||||
<label class="control-label">API Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.deSecToken" name="deSecToken" ng-required="dnsCredentials.provider === 'desec'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
|
||||
<!-- Wildcard -->
|
||||
<p class="small text-info" ng-show="dnsCredentials.provider === 'wildcard'">
|
||||
<span>Set up A records for <b>*.{{ dnsCredentials.domain || 'example.com' }}.</b> and <b>{{ dnsCredentials.domain || 'example.com' }}.</b> to this server's IP.</span>
|
||||
</p>
|
||||
|
||||
<!-- Manual -->
|
||||
<p class="small text-info" ng-show="dnsCredentials.provider === 'manual'">
|
||||
<span>Set up an A record for <b>my.{{ dnsCredentials.domain || 'example.com' }}.</b> to this server's IP.<br/></span>
|
||||
</p>
|
||||
|
||||
<p class="small text-info" ng-show="needsPort80(dnsCredentials.provider, dnsCredentials.tlsConfig.provider)">Let's Encrypt requires your server to be reachable on port 80</p>
|
||||
|
||||
<div ng-show="provider === 'ami'">
|
||||
<h3 class="text-center">Owner verification</h3>
|
||||
<p class="has-error text-center" ng-show="error.ami">{{ error.ami }}</p>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 0;" ng-class="{ 'has-error': dnsCredentialsForm.instanceId.$dirty && (dnsCredentialsForm.instanceId.$invalid || error.ami) }">
|
||||
<label class="control-label">EC2 Instance Id</label>
|
||||
<input type="text" class="form-control" ng-model="instanceId" id="inputInstanceId" name="instanceId" placeholder="i-0123456789abcdefg" ng-minlength="1" ng-required="provider === 'ami'" autocomplete="off">
|
||||
</div>
|
||||
<p style="margin-top: 5px; font-size: 13px;">Provide the EC2 instance id to verify you have access to this server.</p>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<div uib-collapse="!advancedVisible">
|
||||
<div class="form-group">
|
||||
<label class="control-label">DNS Zone Name (Optional) <sup><a ng-href="https://docs.cloudron.io/domains/#zone-name" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.zoneName" name="zoneName" placeholder="Defaults to TLD" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">Certificate Provider <sup><a ng-href="https://docs.cloudron.io/certificates/#certificate-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="dnsCredentials.tlsConfig.provider" ng-options="a.value as a.name for a in tlsProvider" ng-disabled="dnsCredentials.busy"></select>
|
||||
</div>
|
||||
|
||||
<!-- IPv4 provider -->
|
||||
<div class="form-group">
|
||||
<label class="control-label">IPv4 Configuration <sup><a ng-href="https://docs.cloudron.io/networking/#ip-configuration" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="ipv4Config.provider" ng-options="a.value as a.name for a in ipProviders"></select>
|
||||
</div>
|
||||
|
||||
<!-- IPv4 Fixed -->
|
||||
<div class="form-group" ng-show="ipv4Config.provider === 'fixed'">
|
||||
<label class="control-label">IPv4 Address</label>
|
||||
<input type="text" class="form-control" ng-model="ipv4Config.ip" name="ipv4" ng-required="ipv4Config.provider === 'fixed'">
|
||||
</div>
|
||||
|
||||
<!-- IPv4 Network Interface -->
|
||||
<div class="form-group" ng-show="ipv4Config.provider === 'network-interface'">
|
||||
<label class="control-label">IPv4 Interface Name</label>
|
||||
<input type="text" class="form-control" ng-model="ipv4Config.ifname" name="ifname4" ng-required="ipv4Config.provider === 'network-interface'">
|
||||
</div>
|
||||
|
||||
<!-- IPv6 provider -->
|
||||
<div class="form-group">
|
||||
<label class="control-label">IPv6 Configuration <sup><a ng-href="https://docs.cloudron.io/networking/#ip-configuration" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="ipv6Config.provider" ng-options="a.value as a.name for a in ipProviders"></select>
|
||||
</div>
|
||||
|
||||
<!-- IPv6 Fixed -->
|
||||
<div class="form-group" ng-show="ipv6Config.provider === 'fixed'">
|
||||
<label class="control-label">IPv6 Address</label>
|
||||
<input type="text" class="form-control" ng-model="ipv6Config.ip" name="ipv6" ng-required="ipv6Config.provider === 'fixed'">
|
||||
</div>
|
||||
|
||||
<!-- IPv6 Network Interface -->
|
||||
<div class="form-group" ng-show="ipv6Config.provider === 'network-interface'">
|
||||
<label class="control-label">IPv6 Interface Name</label>
|
||||
<input type="text" class="form-control" ng-model="ipv6Config.ifname" name="ifname6" ng-required="ipv6Config.provider === 'network-interface'">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<a href="" ng-click="advancedVisible = true" ng-hide="advancedVisible">Advanced settings...</a>
|
||||
<a href="" ng-click="advancedVisible = false" ng-show="advancedVisible">Hide Advanced settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<button type="submit" class="btn btn-success" ng-disabled="ownerForm.$invalid || owner.busy"><i class="fa fa-circle-notch fa-spin" ng-show="owner.busy"></i> Create Admin</button>
|
||||
<button type="submit" class="btn btn-primary" ng-disabled="dnsCredentialsForm.$invalid"><i class="fa fa-circle-notch fa-spin" ng-show="dnsCredentials.busy"></i> Next</button>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center"><small>Looking to <a ng-href="/restore.html{{ search }}">restore?</a></small></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="view === 'finished'">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="card" style="max-width: none; padding: 20px 40px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<h1>Cloudron is ready to use</h1>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
Before you start:
|
||||
<ul class="fa-ul">
|
||||
<li><i class="fa-li fa fa-users"></i>
|
||||
<b>User management</b>: Cloudron has a central user directory. When installing an app,
|
||||
you can set it up to authenticate against this directory.
|
||||
</li>
|
||||
<br/>
|
||||
<li><i class="fa-li fa fa-envelope-open"></i>
|
||||
<b>Email Configuration</b>: Apps are configured to send email based on the settings in the Email view.
|
||||
This saves you the trouble of having to configure mail settings inside each app.
|
||||
</li>
|
||||
<br/>
|
||||
<li><i class="fa-li fa fa-archive"></i>
|
||||
<b>Backups</b>: Store your backups on storage services completely independent from your server.
|
||||
You can use backups to seamlessly migrate your setup to another server.
|
||||
</li>
|
||||
<br/>
|
||||
<li><i class="fa-li fa fa-birthday-cake"></i>
|
||||
<b>Updates</b>: The Cloudron team tracks upstream releases and publishes app updates after testing.
|
||||
Your apps are kept fresh & secure.
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<a class="btn btn-success" href="/">Proceed to Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-center">
|
||||
<footer class="text-center" ng-show="state === 'waitingForDnsSetup' || state === 'initialized'">
|
||||
<span class="text-muted">©2022 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted"><a href="https://forum.cloudron.io" target="_blank">Forum <i class="fa fa-comments"></i></a></span>
|
||||
</footer>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css?<%= revision %>">
|
||||
|
||||
<!-- Fontawesome -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.css?<%= revision %>"/>
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.min.css?<%= revision %>"/>
|
||||
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js?<%= revision %>"></script>
|
||||
@@ -147,7 +147,7 @@
|
||||
<br/>
|
||||
<h2>{{ 'setupAccount.success.title' | tr }}</h2>
|
||||
<br/>
|
||||
<a href="/" class="btn btn-primary">{{ 'setupAccount.success.openDashboardAction' | tr }}</a>
|
||||
<a ng-href="dashboardUrl" class="btn btn-primary">{{ 'setupAccount.success.openDashboardAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,335 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<title>Cloudron Domain Setup</title>
|
||||
<meta name="description" content="Cloudron Domain Setup">
|
||||
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css">
|
||||
|
||||
<!-- Fontawesome -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.css?<%= revision %>"/>
|
||||
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js"></script>
|
||||
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js"></script>
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js"></script>
|
||||
|
||||
<!-- Angularjs scripts -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/autofill-event.js"></script>
|
||||
|
||||
<!-- Angular directives for tldjs -->
|
||||
<script type="text/javascript" src="/3rdparty/js/tld.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/clipboard.min.js"></script>
|
||||
|
||||
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js"></script>
|
||||
|
||||
<!-- Angular translate https://angular-translate.github.io/ -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-loader-static-files.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-cookie.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-local.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Setup Application -->
|
||||
<script type="text/javascript" src="/js/setupdns.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="setup" ng-app="Application" ng-controller="SetupDNSController">
|
||||
|
||||
<div class="main-container ng-cloak text-center" ng-show="state === 'waitingForDnsSetup' || state === 'waitingForBox'">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3 text-center">
|
||||
<i class="fa fa-circle-notch fa-spin fa-5x"></i><br/>
|
||||
<h3>{{ message }} ...</h3>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<p>
|
||||
Please wait while Cloudron is setting up the dashboard at my.{{dnsCredentials.domain}}.<br/>
|
||||
You can follow the logs on the server at <code class="clipboard hand" data-clipboard-text="/home/yellowtent/platformdata/logs/box.log" uib-tooltip="{{ clipboardDone ? 'Copied' : 'Click to copy' }}" tooltip-placement="right">/home/yellowtent/platformdata/logs/box.log</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-container ng-cloak" ng-show="state === 'initialized'">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="card" style="max-width: none; padding: 20px;">
|
||||
<form name="dnsCredentialsForm" role="form" novalidate ng-submit="setDnsCredentials()" autocomplete="off">
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1 text-center">
|
||||
<h1>Domain Setup</h1>
|
||||
<p class="has-error text-center" ng-show="error.setup">{{ error.setup }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="form-group" style="margin-bottom: 0;" ng-class="{ 'has-error': dnsCredentialsForm.domain.$dirty && dnsCredentialsForm.domain.$invalid }">
|
||||
<label class="control-label">Domain <sup><a ng-href="https://docs.cloudron.io/installation/#domain-setup" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.domain" name="domain" placeholder="example.com" required autofocus ng-disabled="dnsCredentials.busy">
|
||||
<div class="text-danger" ng-show="dnsCredentials.domain.indexOf('my.') === 0 && dnsCredentials.domain.length > 3">Are you sure about this domain? The dashboard will be at <b>my.{{ dnsCredentials.domain }}</b></div>
|
||||
<p style="margin-top: 5px; font-size: 13px;">
|
||||
Apps will be installed on subdomains of this domain. The dashboard will be available on the <b>my</b> subdomain. You can add more domains later.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<h3 class="text-center">Domain Configuration <sup><a ng-href="https://docs.cloudron.io/domains/#dns-providers" class="help" target="_blank" tabindex="-1"><i class="fa fa-question-circle"></i></a></sup> </h3>
|
||||
<p class="has-error text-center" ng-show="error.dnsCredentials">{{ error.dnsCredentials }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">DNS Provider</label>
|
||||
<select class="form-control" ng-model="dnsCredentials.provider" ng-options="a.value as a.name for a in dnsProvider" ng-disabled="dnsCredentials.busy" ng-change="setDefaultTlsProvider()"></select>
|
||||
</div>
|
||||
|
||||
<!-- Route53 -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.accessKeyId.$dirty && dnsCredentialsForm.accessKeyId.$invalid }" ng-show="dnsCredentials.provider === 'route53'">
|
||||
<label class="control-label">Access Key Id</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.accessKeyId" name="accessKeyId" placeholder="Access Key Id" ng-minlength="16" ng-maxlength="32" ng-required="dnsCredentials.provider === 'route53'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.secretAccessKey.$dirty && dnsCredentialsForm.secretAccessKey.$invalid }" ng-show="dnsCredentials.provider === 'route53'">
|
||||
<label class="control-label">Secret Access Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.secretAccessKey" name="secretAccessKey" placeholder="Secret Access Key" ng-required="dnsCredentials.provider === 'route53'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Google Cloud DNS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="dnsCredentials.provider === 'gcdns'">
|
||||
<label class="control-label">Service Account Key</label>
|
||||
<div class="input-group">
|
||||
<input type="file" id="gcdnsKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="dnsCredentials.gcdnsKey.keyFileName" id="gcdnsKeyInput" name="cert" onclick="getElementById('gcdnsKeyFileInput').click();" style="cursor: pointer;" ng-required="dnsCredentials.provider === 'gcdns'" ng-disabled="dnsCredentials.busy">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('gcdnsKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DigitalOcean -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.digitalOceanToken.$dirty && dnsCredentialsForm.digitalOceanToken.$invalid }" ng-show="dnsCredentials.provider === 'digitalocean'">
|
||||
<label class="control-label">DigitalOcean Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.digitalOceanToken" name="digitalOceanToken" placeholder="API Token" ng-required="dnsCredentials.provider === 'digitalocean'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Gandi -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.gandiApiKey.$dirty && dnsCredentialsForm.gandiApiKey.$invalid }" ng-show="dnsCredentials.provider === 'gandi'">
|
||||
<label class="control-label">Gandi API Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.gandiApiKey" name="gandiApiKey" placeholder="API Key" ng-required="dnsCredentials.provider === 'gandi'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- GoDaddy -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.godaddyApiKey.$dirty && dnsCredentialsForm.godaddyApiKey.$invalid }" ng-show="dnsCredentials.provider === 'godaddy'">
|
||||
<label class="control-label">API Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.godaddyApiKey" name="godaddyApiKey" placeholder="API Key" ng-minlength="1" ng-required="dnsCredentials.provider === 'godaddy'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.godaddyApiSecret.$dirty && dnsCredentialsForm.godaddyApiSecret.$invalid }" ng-show="dnsCredentials.provider === 'godaddy'">
|
||||
<label class="control-label">API Secret</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.godaddyApiSecret" name="godaddyApiSecret" placeholder="API Secret" ng-required="dnsCredentials.provider === 'godaddy'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Netcup -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.netcupCustomerNumber.$dirty && dnsCredentialsForm.netcupCustomerNumber.$invalid }" ng-show="dnsCredentials.provider === 'netcup'">
|
||||
<label class="control-label">Customer Number</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.netcupCustomerNumber" name="netcupCustomerNumber" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'netcup'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.netcupApiKey.$dirty && dnsCredentialsForm.netcupApiKey.$invalid }" ng-show="dnsCredentials.provider === 'netcup'">
|
||||
<label class="control-label">API Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.netcupApiKey" name="netcupApiKey" ng-disabled="dnsCredentials.busy" ng-minlength="1" ng-required="dnsCredentials.provider === 'netcup'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.netcupApiPassword.$dirty && dnsCredentialsForm.netcupApiPassword.$invalid }" ng-show="dnsCredentials.provider === 'netcup'">
|
||||
<label class="control-label">API Password</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.netcupApiPassword" name="netcupApiPassword" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'netcup'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Cloudflare -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.cloudflareToken.$dirty && dnsCredentialsForm.cloudflareToken.$invalid }" ng-show="dnsCredentials.provider === 'cloudflare'">
|
||||
<label class="control-label">Token Type</label>
|
||||
<select class="form-control" ng-model="dnsCredentials.cloudflareTokenType">
|
||||
<option value="GlobalApiKey">Global API Key</option>
|
||||
<option value="ApiToken">API Token</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.cloudflareToken.$dirty && dnsCredentialsForm.cloudflareToken.$invalid }" ng-show="dnsCredentials.provider === 'cloudflare'">
|
||||
<label class="control-label" ng-show="dnsCredentials.cloudflareTokenType === 'GlobalApiKey'">Global API Key</label>
|
||||
<label class="control-label" ng-show="dnsCredentials.cloudflareTokenType === 'ApiToken'">Api Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.cloudflareToken" name="cloudflareToken" placeholder="API Key/Token" ng-required="dnsCredentials.provider === 'cloudflare'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.cloudflareEmail.$dirty && dnsCredentialsForm.cloudflareEmail.$invalid }" ng-show="dnsCredentials.provider === 'cloudflare' && dnsCredentials.cloudflareTokenType === 'GlobalApiKey'">
|
||||
<label class="control-label">Cloudflare Email</label>
|
||||
<input type="email" class="form-control" ng-model="dnsCredentials.cloudflareEmail" name="cloudflareEmail" placeholder="Cloudflare Account Email" ng-required="dnsCredentials.provider === 'cloudflare' && dnsCredentials.cloudflareTokenType === 'GlobalApiKey'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="dnsCredentials.provider === 'cloudflare'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="dnsCredentials.cloudflareDefaultProxyStatus"> Enable proxying for new DNS records
|
||||
<sup><a ng-href="https://docs.cloudron.io/domains/#cloudflare-dns" class="help" target="_blank" tabIndex="-1"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Name.com -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.nameComUsername.$dirty && dnsCredentialsForm.nameComUsername.$invalid }" ng-show="dnsCredentials.provider === 'namecom'">
|
||||
<label class="control-label">Name.com Username</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.nameComUsername" name="nameComUsername" placeholder="Name.com Username" ng-required="dnsCredentials.provider === 'namecom'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.nameComToken.$dirty && dnsCredentialsForm.nameComToken.$invalid }" ng-show="dnsCredentials.provider === 'namecom'">
|
||||
<label class="control-label">API Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.nameComToken" name="nameComToken" placeholder="Name.com API Token" ng-required="dnsCredentials.provider === 'namecom'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Namecheap -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.namecheapUsername.$dirty && dnsCredentialsForm.namecheapUsername.$invalid }" ng-show="dnsCredentials.provider === 'namecheap'">
|
||||
<label class="control-label">Namecheap Username</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.namecheapUsername" name="namecheapUsername" placeholder="Namecheap Username" ng-required="dnsCredentials.provider === 'namecheap'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.namecheapApiKey.$dirty && dnsCredentialsForm.namecheapApiKey.$invalid }" ng-show="dnsCredentials.provider === 'namecheap'">
|
||||
<label class="control-label">API Key</label>
|
||||
<p class="small text-info" ng-show="dnsCredentials.provider === 'namecheap'"><b>The server IP needs to be whitelisted for this API Key.</b></p>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.namecheapApiKey" name="namecheapApiKey" placeholder="Namecheap API Key" ng-required="dnsCredentials.provider === 'namecheap'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Linode -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'linode'">
|
||||
<label class="control-label">API Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.linodeToken" name="linodeToken" ng-required="dnsCredentials.provider === 'linode'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
|
||||
<!-- Bunny -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'bunny'">
|
||||
<label class="control-label">Access Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.bunnyAccessKey" name="bunnyAccessKey" ng-required="dnsCredentials.provider === 'bunny'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
|
||||
<!-- Porkbun -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.porkbunApikey.$dirty && dnsCredentialsForm.porkbunApikey.$invalid }" ng-show="dnsCredentials.provider === 'porkbun'">
|
||||
<label class="control-label">API Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.porkbunApikey" name="porkbunApikey" placeholder="API Key" ng-minlength="1" ng-required="dnsCredentials.provider === 'porkbun'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.porkbunSecretapikey.$dirty && dnsCredentialsForm.porkbunSecretapikey.$invalid }" ng-show="dnsCredentials.provider === 'porkbun'">
|
||||
<label class="control-label">API Secret</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.porkbunSecretapikey" name="porkbunSecretapikey" placeholder="API Secret" ng-required="dnsCredentials.provider === 'porkbun'" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<!-- Hetzner -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'hetzner'">
|
||||
<label class="control-label">API Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.hetznerToken" name="hetznerToken" ng-required="dnsCredentials.provider === 'hetzner'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
|
||||
<!-- Vultr -->
|
||||
<p class="form-group" ng-show="dnsCredentials.provider === 'vultr'">
|
||||
<label class="control-label">API Token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.vultrToken" name="vultrToken" ng-required="dnsCredentials.provider === 'vultr'" ng-disabled="dnsCredentials.busy">
|
||||
</p>
|
||||
|
||||
<!-- Wildcard -->
|
||||
<p class="small text-info" ng-show="dnsCredentials.provider === 'wildcard'">
|
||||
<span>Set up A records for <b>*.{{ dnsCredentials.domain || 'example.com' }}.</b> and <b>{{ dnsCredentials.domain || 'example.com' }}.</b> to this server's IP.</span>
|
||||
</p>
|
||||
|
||||
<!-- Manual -->
|
||||
<p class="small text-info" ng-show="dnsCredentials.provider === 'manual'">
|
||||
<span>Set up an A record for <b>my.{{ dnsCredentials.domain || 'example.com' }}.</b> to this server's IP.<br/></span>
|
||||
</p>
|
||||
|
||||
<p class="small text-info" ng-show="needsPort80(dnsCredentials.provider, dnsCredentials.tlsConfig.provider)">Let's Encrypt requires your server to be reachable on port 80</p>
|
||||
|
||||
<div ng-show="provider === 'ami'">
|
||||
<h3 class="text-center">Owner verification</h3>
|
||||
<p class="has-error text-center" ng-show="error.ami">{{ error.ami }}</p>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 0;" ng-class="{ 'has-error': dnsCredentialsForm.instanceId.$dirty && (dnsCredentialsForm.instanceId.$invalid || error.ami) }">
|
||||
<label class="control-label">EC2 Instance Id</label>
|
||||
<input type="text" class="form-control" ng-model="instanceId" id="inputInstanceId" name="instanceId" placeholder="i-0123456789abcdefg" ng-minlength="1" ng-required="provider === 'ami'" autocomplete="off">
|
||||
</div>
|
||||
<p style="margin-top: 5px; font-size: 13px;">Provide the EC2 instance id to verify you have access to this server.</p>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<div uib-collapse="!advancedVisible">
|
||||
<div class="form-group">
|
||||
<label class="control-label">Zone Name (Optional) <sup><a ng-href="https://docs.cloudron.io/domains/#zone-name" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.zoneName" name="zoneName" placeholder="{{dnsCredentials.domain | zoneName}}" ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">Certificate Provider <sup><a ng-href="https://docs.cloudron.io/certificates/#certificate-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="dnsCredentials.tlsConfig.provider" ng-options="a.value as a.name for a in tlsProvider" ng-disabled="dnsCredentials.busy"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">IP Configuration <sup><a ng-href="https://docs.cloudron.io/networking/#ip-configuration" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="sysinfo.provider" ng-options="a.value as a.name for a in sysinfoProvider"></select>
|
||||
</div>
|
||||
|
||||
<!-- Fixed -->
|
||||
<div class="form-group" ng-show="sysinfo.provider === 'fixed'" ng-class="{ 'has-error': error.ipv4 }">
|
||||
<label class="control-label">IP Address</label>
|
||||
<input type="text" class="form-control" ng-model="sysinfo.ipv4" name="ipv4" ng-disabled="sysinfo.busy" ng-required="sysinfo.provider === 'fixed'">
|
||||
<p class="has-error" ng-show="error.ipv4">{{ error.ipv4 }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Network Interface -->
|
||||
<div class="form-group" ng-show="sysinfo.provider === 'network-interface'" ng-class="{ 'has-error': error.ifname }">
|
||||
<label class="control-label">Interface Name</label>
|
||||
<input type="text" class="form-control" ng-model="sysinfo.ifname" name="ifname" ng-disabled="sysinfo.busy" ng-required="sysinfo.provider === 'network-interface'">
|
||||
<p class="has-error" ng-show="error.ifname">{{ error.ifname }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<a href="" ng-click="advancedVisible = true" ng-hide="advancedVisible">Advanced settings...</a>
|
||||
<a href="" ng-click="advancedVisible = false" ng-show="advancedVisible">Hide Advanced settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<button type="submit" class="btn btn-primary" ng-disabled="dnsCredentialsForm.$invalid"><i class="fa fa-circle-notch fa-spin" ng-show="dnsCredentials.busy"></i> Next</button>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center"><small>Looking to <a ng-href="/restore.html{{ search }}">restore?</a></small></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted">©2022 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted"><a href="https://forum.cloudron.io" target="_blank">Forum <i class="fa fa-comments"></i></a></span>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,120 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<title> Login to <%= title %> </title>
|
||||
|
||||
<link href="<%= icon %>" rel="icon" type="image/png">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="<%= dashboardOrigin %>/theme.css">
|
||||
|
||||
<!-- Fontawesome -->
|
||||
<link type="text/css" rel="stylesheet" href="<%= dashboardOrigin %>/3rdparty/fontawesome/css/all.css"/>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="layout-root">
|
||||
|
||||
<div class="layout-content">
|
||||
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" style="margin-top: -84px" src="<%= icon %>"/>
|
||||
<br/>
|
||||
<h1><small>{{ login.loginTo }}</small> <%= title %></h1>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4 class="has-error" id="message"></h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form name="loginForm" onsubmit="return onLogin(event)">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputUsername">{{ login.username }}</label>
|
||||
<input type="text" class="form-control" id="inputUsername" name="username" autofocus required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputPassword">{{ login.password }}</label>
|
||||
<input type="password" class="form-control" name="password" id="inputPassword" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputTotpToken">{{ login.2faToken }}</label>
|
||||
<input type="text" class="form-control" name="totpToken" id="inputTotpToken" value="">
|
||||
</div>
|
||||
<button class="btn btn-primary btn-outline pull-right" type="submit" id="login">{{ login.signInAction }}</button>
|
||||
</form>
|
||||
<!-- <a ng-href="" class="hand" ng-click="showPasswordReset()">Reset password</a> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
function onLogin(event) {
|
||||
event.preventDefault();
|
||||
|
||||
var username = document.getElementById('inputUsername').value;
|
||||
var password = document.getElementById('inputPassword').value;
|
||||
var totpToken = document.getElementById('inputTotpToken').value;
|
||||
|
||||
fetch('/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
redirect: 'manual',
|
||||
body: JSON.stringify({ username: username, password: password, totpToken: totpToken })
|
||||
}).then(function (response) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
document.getElementById('message').innerText = "{{ login.errorIncorrectCredentials }}"; // FIXME this needs proper escaping for translated strings, single quotes break easily!
|
||||
return;
|
||||
}
|
||||
|
||||
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
window.location.href = search.redirect || '/';
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// patch up for password reveal see dashboard/js/utils.js
|
||||
var element = document.getElementById('inputPassword');
|
||||
|
||||
var svgEye = '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye" class="svg-inline--fa fa-eye fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z"></path></svg>';
|
||||
var svgEyeSlash = '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye-slash" class="svg-inline--fa fa-eye-slash fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1l-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142l-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z"></path></svg>';
|
||||
|
||||
var eye = document.createElement('i');
|
||||
eye.innerHTML = svgEyeSlash;
|
||||
eye.style.width = '18px';
|
||||
eye.style.height = '18px';
|
||||
eye.style.position = 'relative';
|
||||
eye.style.float = 'right';
|
||||
eye.style.marginTop = '-24px';
|
||||
eye.style.marginRight = '10px';
|
||||
eye.style.cursor = 'pointer';
|
||||
|
||||
eye.addEventListener('click', function () {
|
||||
if (element.type === 'password') {
|
||||
element.type = 'text';
|
||||
eye.innerHTML = svgEye;
|
||||
} else {
|
||||
element.type = 'password';
|
||||
eye.innerHTML = svgEyeSlash;
|
||||
}
|
||||
});
|
||||
|
||||
element.parentNode.style.position = 'relative';
|
||||
element.parentNode.insertBefore(eye, element.nextSibling);
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+206
-75
@@ -9,13 +9,13 @@ $brand-info: #3995b1 !default;
|
||||
$brand-warning: #f0ad4e !default;
|
||||
$brand-danger: #ff4c4c !default;
|
||||
|
||||
$body-bg: #E5E5E5;
|
||||
$font-family-sans-serif: Roboto, Helvetica, Arial, sans-serif;
|
||||
$font-family-heading: Roboto-Light, Helvetica, Arial, sans-serif;
|
||||
$body-bg: #f4f4f4;
|
||||
$font-family-sans-serif: "Noto Sans", Helvetica, Arial, sans-serif;
|
||||
$font-family-heading: "Noto Sans Light", Helvetica, Arial, sans-serif;
|
||||
|
||||
$navbar-default-link-color: #428BCA !default;
|
||||
$navbar-default-link-color: $brand-primary !default;
|
||||
$navbar-default-link-hover-color: #62bdfc !default;
|
||||
$navbar-default-link-active-color: #62bdfc !default;
|
||||
$navbar-default-link-active-color: #428BCA !default;
|
||||
$navbar-default-brand-color: #777 !default;
|
||||
|
||||
$btn-default-bg: transparent !default;
|
||||
@@ -54,21 +54,28 @@ $state-danger-text: $brand-danger;
|
||||
$state-danger-border: $brand-danger;
|
||||
|
||||
@import "bootstrap";
|
||||
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
src: url(3rdparty/Roboto-Regular.ttf);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Roboto-Light;
|
||||
src: url(3rdparty/Roboto-Light.ttf);
|
||||
}
|
||||
@import "3rdparty/noto-sans/index.css";
|
||||
|
||||
// ----------------------------
|
||||
// Bootstrap extension
|
||||
// ----------------------------
|
||||
|
||||
h1, h2, h3, h4, h5, h6,
|
||||
.h1, .h2, .h3, .h4, .h5, .h6 {
|
||||
font-family: $font-family-heading;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.hide-mobile {
|
||||
@media(max-width:767px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.table-hover > tbody > tr:hover {
|
||||
background-color: $body-bg;
|
||||
}
|
||||
|
||||
.text-monospace {
|
||||
font-family: $font-family-monospace;
|
||||
}
|
||||
@@ -105,6 +112,7 @@ $state-danger-border: $brand-danger;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
max-width: 300px;
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
.wrap-table-cell {
|
||||
@@ -169,16 +177,7 @@ html, body {
|
||||
}
|
||||
|
||||
.view-header-filter-bar {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
margin-top: 5px;
|
||||
padding: 5px;
|
||||
padding-top: 0;
|
||||
background-color: #fff;
|
||||
background-clip: padding-box;
|
||||
border: 1px solid rgba(0,0,0,.15);
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 6px 12px rgba(0,0,0,.175);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.view-header-search-bar {
|
||||
@@ -272,9 +271,11 @@ html, body {
|
||||
display: block;
|
||||
width: 100%;
|
||||
flex-grow: 0;
|
||||
background-color: white;
|
||||
border-color: white;
|
||||
|
||||
.navbar-collapse {
|
||||
background-color: #F8F8F8;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
@media(min-width:768px) {
|
||||
@@ -354,6 +355,51 @@ textarea {
|
||||
// Apps view
|
||||
// ----------------------------
|
||||
|
||||
.app-list {
|
||||
width: 100%;
|
||||
margin-top: 20px !important;
|
||||
|
||||
th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-list-item {
|
||||
.app-list-item-icon {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.app-list-app-link-cell {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.app-list-item-fqdn {
|
||||
visibility: hidden;
|
||||
color: $text-muted;
|
||||
margin-left: 20px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&:hover .app-list-item-fqdn {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.app-list-app-link {
|
||||
display: inline-block;
|
||||
color: $text-dark;
|
||||
padding: 8px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.app-list-item-progress {
|
||||
height: 5px;
|
||||
margin: 0 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -488,6 +534,28 @@ textarea {
|
||||
}
|
||||
}
|
||||
|
||||
.app-checklist-badge {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
right: -12px;
|
||||
top: -12px;
|
||||
z-index: 2;
|
||||
font-size: 14px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
background-color: $brand-danger;
|
||||
border-radius: 34px;
|
||||
transition: all 100ms ease-out;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
}
|
||||
|
||||
.app-postinstall-message {
|
||||
max-height: 500px;
|
||||
overflow-x: none;
|
||||
@@ -500,11 +568,6 @@ textarea {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.app-info-meta {
|
||||
margin-left: 4px;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.app-info-icon {
|
||||
float: left;
|
||||
min-height: 64px;
|
||||
@@ -540,6 +603,7 @@ multiselect {
|
||||
cursor: pointer;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: 5px;
|
||||
background-position: center;
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
@@ -606,6 +670,10 @@ multiselect {
|
||||
padding: 10px;
|
||||
background-color: white;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background-color: #1c1c1c;
|
||||
}
|
||||
|
||||
@media(min-width:768px) {
|
||||
background-color: transparent;
|
||||
width: auto;
|
||||
@@ -629,6 +697,22 @@ multiselect {
|
||||
}
|
||||
}
|
||||
|
||||
.checklist-item {
|
||||
padding: 8px;
|
||||
border: none;
|
||||
border-left: 2px solid rgb(255, 76, 76);
|
||||
background-color: #ff000014;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.checklist-item-acknowledged {
|
||||
border-left: 2px solid $brand-success;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Mail view
|
||||
// ----------------------------
|
||||
@@ -689,7 +773,7 @@ multiselect {
|
||||
}
|
||||
|
||||
.card {
|
||||
min-height: 523px;
|
||||
min-height: 558px;
|
||||
}
|
||||
|
||||
@media(min-width:768px) {
|
||||
@@ -715,11 +799,14 @@ multiselect {
|
||||
|
||||
h1 {
|
||||
margin-right: 10px;
|
||||
line-height: 0.7;
|
||||
margin-bottom: 0px;
|
||||
margin-top: 16px;
|
||||
padding: 4px 0;
|
||||
line-height: 1;
|
||||
font-size: 30px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
overflow: hidden visible;
|
||||
|
||||
a {
|
||||
white-space: nowrap;
|
||||
@@ -795,7 +882,7 @@ multiselect {
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
background-color: #f3f3f3;
|
||||
background-color: #e9ebed;
|
||||
box-shadow: -4px 3px 5px -2px rgba(0,0,0,.1);
|
||||
}
|
||||
|
||||
@@ -867,7 +954,7 @@ multiselect {
|
||||
.appstore-toolbar-content {
|
||||
display: flex;
|
||||
margin: auto;
|
||||
max-width: 1200px;
|
||||
max-width: 1400px;
|
||||
|
||||
> * {
|
||||
margin: 0 10px;
|
||||
@@ -905,7 +992,7 @@ multiselect {
|
||||
margin: auto;
|
||||
overflow: auto;
|
||||
height: calc(100% - 65px); // offset navigation bar
|
||||
max-width: 1200px;
|
||||
max-width: 1400px;
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
@@ -943,6 +1030,11 @@ multiselect {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0px;
|
||||
margin-top: 16px;
|
||||
padding: 4px 0;
|
||||
line-height: 1;
|
||||
font-family: $font-family-sans-serif;
|
||||
}
|
||||
|
||||
.appstore-item-content-tagline {
|
||||
@@ -952,11 +1044,18 @@ multiselect {
|
||||
}
|
||||
|
||||
.appstore-item-content-icon {
|
||||
width: 100px;
|
||||
min-width: 100px;
|
||||
max-width: 100px;
|
||||
width: 90px;
|
||||
min-width: 90px;
|
||||
max-width: 90px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
|
||||
> .app-icon {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
min-width: 70px;
|
||||
min-height: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.appstore-category-link {
|
||||
@@ -1078,6 +1177,10 @@ multiselect {
|
||||
margin-bottom: 15px;
|
||||
padding: 10px 15px;
|
||||
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1);
|
||||
|
||||
@media(max-width:767px) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card-small {
|
||||
@@ -1088,6 +1191,10 @@ multiselect {
|
||||
max-width: 970px;
|
||||
}
|
||||
|
||||
.card-expand {
|
||||
max-width: initial;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #5CB85C;
|
||||
}
|
||||
@@ -1300,7 +1407,7 @@ select.purpose:invalid {
|
||||
|
||||
footer {
|
||||
flex-grow: 0;
|
||||
background-color: #f8f8f8;
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
color: #555;
|
||||
max-height: 30px;
|
||||
@@ -1426,6 +1533,39 @@ footer {
|
||||
// Settings
|
||||
// ----------------------------
|
||||
|
||||
.picture-edit-indicator {
|
||||
position: absolute;
|
||||
bottom: -4px;
|
||||
right: -4px;
|
||||
border-radius: 20px;
|
||||
padding: 5px;
|
||||
color: $text-dark;
|
||||
background-color: white;
|
||||
transition: all 250ms;
|
||||
}
|
||||
|
||||
div:hover > .picture-edit-indicator {
|
||||
color: white;
|
||||
background: $brand-primary;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.info-edit-indicator {
|
||||
float: right;
|
||||
border-radius: 20px;
|
||||
padding: 5px;
|
||||
color: $text-dark;
|
||||
background-color: white;
|
||||
transition: all 250ms;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.info-edit-indicator:hover {
|
||||
color: white;
|
||||
background: $brand-primary;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.settings-avatar {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
@@ -1443,23 +1583,6 @@ footer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(127, 127, 127 ,0.3);
|
||||
background-image: url('/img/plus.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
transition: all 150ms;
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-avatar-selector {
|
||||
@@ -1666,23 +1789,6 @@ footer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(127, 127, 127 ,0.3);
|
||||
background-image: url('/img/plus.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
transition: all 150ms;
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.branding-avatar-selector {
|
||||
@@ -1727,6 +1833,23 @@ footer {
|
||||
}
|
||||
}
|
||||
|
||||
.branding-background {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
width: 256px;
|
||||
background-position: center;
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
border: 1px solid gray;
|
||||
border-radius: 3px;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Tag Input
|
||||
// ----------------------------
|
||||
@@ -2205,6 +2328,14 @@ tag-input {
|
||||
}
|
||||
}
|
||||
|
||||
.app-list .app-list-item .app-list-app-link {
|
||||
color: $textColor;
|
||||
}
|
||||
|
||||
.app-list .app-list-item:hover .app-list-app-link {
|
||||
color: white;
|
||||
}
|
||||
|
||||
footer, .card, .app-configure-links div.active {
|
||||
background-color: $backgroundDark;
|
||||
}
|
||||
|
||||
@@ -182,7 +182,6 @@
|
||||
"title": "Tilslut en ekstern mappe",
|
||||
"description": "Cloudron synkroniserer brugere og grupper fra en ekstern LDAP- eller ActiveDirectory-server. Adgangskodebekræftelse til autentificering af disse brugere foretages mod den eksterne server. Synkroniseringen køres ikke automatisk, men skal udløses manuelt.",
|
||||
"bindUsername": "Bind DN/Benyttelsesnavn (valgfrit)",
|
||||
"subscriptionRequired": "Denne funktion er kun tilgængelig i de betalte abonnementer.",
|
||||
"subscriptionRequiredAction": "Oprettelse af abonnement nu",
|
||||
"noopInfo": "LDAP-godkendelse er ikke konfigureret.",
|
||||
"provider": "Udbyder",
|
||||
@@ -271,12 +270,6 @@
|
||||
"failed": "Følgende brugere blev ikke importeret:",
|
||||
"sendInviteCheckbox": "Send en e-mail med invitation til importerede brugere"
|
||||
},
|
||||
"makeLocalDialog": {
|
||||
"description": "Dette vil migrere brugeren fra den eksterne mappe til Cloudron.",
|
||||
"title": "Gør denne bruger lokal",
|
||||
"warning": "En nulstilling af adgangskode vil blive iværksat for at indstille en lokal adgangskode for denne bruger.",
|
||||
"submitAction": "Gør lokale"
|
||||
},
|
||||
"title": "Brugerkatalog",
|
||||
"newUserAction": "Ny bruger",
|
||||
"users": {
|
||||
@@ -296,7 +289,6 @@
|
||||
"invitationTooltip": "Inviter bruger",
|
||||
"mailmanagerTooltip": "Denne bruger kan administrere brugere og postkasser",
|
||||
"count": "Antal brugere i alt: {{ count }}",
|
||||
"makeLocalTooltip": "Gør brugeren lokal",
|
||||
"setGhostTooltip": "Udgive sig for at være"
|
||||
},
|
||||
"groups": {
|
||||
|
||||
@@ -22,13 +22,17 @@
|
||||
"auth": {
|
||||
"nosso": "Die App verwendet eine eigene Benutzerverwaltung",
|
||||
"email": "Mit E-Mail-Adresse anmelden",
|
||||
"sso": "Mit Cloudron Zugangsdaten anmelden"
|
||||
"sso": "Mit Cloudron Zugangsdaten anmelden",
|
||||
"openid": "Mit Cloudron OpenID anmelden"
|
||||
},
|
||||
"addAppAction": "App hinzufügen",
|
||||
"addAppproxyAction": "App Proxy hinzufügen",
|
||||
"addApplinkAction": "App Link hinzufügen",
|
||||
"filter": {
|
||||
"clearAll": "Alles löschen"
|
||||
},
|
||||
"apps": {
|
||||
"count": "Appanzahl: {{ count }}"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
@@ -51,7 +55,8 @@
|
||||
},
|
||||
"action": {
|
||||
"logs": "Logs",
|
||||
"reboot": "Neustarten"
|
||||
"reboot": "Neustarten",
|
||||
"showLogs": "Zeige Logs"
|
||||
},
|
||||
"pagination": {
|
||||
"perPageSelector": "Zeige {{ n }} pro Seite",
|
||||
@@ -79,7 +84,8 @@
|
||||
"justNow": "gerade eben",
|
||||
"yeserday": "Gestern",
|
||||
"minutesAgo": "vor {{ m }} Minuten",
|
||||
"hoursAgo": "vor {{ h }} Stunden"
|
||||
"hoursAgo": "vor {{ h }} Stunden",
|
||||
"never": "Nie"
|
||||
},
|
||||
"disableAction": "Deaktivieren",
|
||||
"enableAction": "Aktivieren",
|
||||
@@ -89,16 +95,18 @@
|
||||
},
|
||||
"statusDisabled": "Deaktiviert",
|
||||
"loadingPlaceholder": "Laden",
|
||||
"settings": "Einstellungen"
|
||||
"settings": "Einstellungen",
|
||||
"saveAction": "Speichern"
|
||||
},
|
||||
"network": {
|
||||
"title": "Netzwerk",
|
||||
"dyndns": {
|
||||
"title": "Dynamischer DNS",
|
||||
"description": "Diese Option aktivieren, um alle DNS-Einträge mit einer sich ändernden IP-Adresse synchron zu halten. Dies ist nützlich, wenn Cloudron in einem Netzwerk mit einer sich häufig ändernden öffentlichen IP-Adresse wie einer Heimverbindung läuft."
|
||||
"description": "Diese Option aktivieren, um alle DNS-Einträge mit einer sich ändernden IP-Adresse synchron zu halten. Dies ist nützlich, wenn Cloudron in einem Netzwerk mit einer sich häufig ändernden öffentlichen IP-Adresse wie einer Heimverbindung läuft.",
|
||||
"showLogsAction": "Zeige Logs"
|
||||
},
|
||||
"configureIp": {
|
||||
"title": "IP-Anbieter konfigurieren",
|
||||
"title": "IPv4-Anbieter konfigurieren",
|
||||
"providerGenericDescription": "Die öffentliche IP-Adresse des Servers wird automatisch erkannt."
|
||||
},
|
||||
"firewall": {
|
||||
@@ -112,12 +120,12 @@
|
||||
"blocklist": "{{ blockCount }} IP(s) sind gesperrt"
|
||||
},
|
||||
"ip": {
|
||||
"description": "Cloudron verwendet diese IP-Adresse beim Einrichten von DNS-Einträgen.",
|
||||
"description": "Cloudron verwendet diese IPv4-Adresse beim Einrichten von DNS A Einträgen.",
|
||||
"provider": "Anbieter",
|
||||
"interface": "Name der Netzwerkschnittstelle",
|
||||
"configure": "Konfigurieren",
|
||||
"interfaceDescription": "Verfügbare Netzwerkgeräte auf dem Server anzeigen mit:",
|
||||
"title": "IP-Adresse",
|
||||
"title": "IPv4",
|
||||
"detected": "ermittelt",
|
||||
"address": "IP Adresse"
|
||||
},
|
||||
@@ -131,7 +139,13 @@
|
||||
},
|
||||
"ipv4": {
|
||||
"address": "IPv4 Adresse"
|
||||
}
|
||||
},
|
||||
"trustedIps": {
|
||||
"description": "HTTP header, von übereinstimmenden IP-Adressen, wird vertraut",
|
||||
"summary": "{{ trustCount }} IPs vertrauen",
|
||||
"title": "Konfiguriere vertrauenswürdige IPs"
|
||||
},
|
||||
"trustedIpRanges": "Vertrauenswürdige IPs & IP-Bereichen "
|
||||
},
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
@@ -167,7 +181,7 @@
|
||||
"description": "Ein Cloudron.io-Konto wird für den Zugriff auf den App-Store und die Verwaltung des Abonnements verwendet.",
|
||||
"subscriptionSetupAction": "Abonnement einrichten",
|
||||
"cloudronId": "Cloudron-ID",
|
||||
"subscriptionChangeAction": "Abonnement ändern",
|
||||
"subscriptionChangeAction": "Abonnement verwalten",
|
||||
"setupAction": "Konto einrichten",
|
||||
"subscription": "Abonnement-Typ",
|
||||
"subscriptionReactivateAction": "Abonnement reaktivieren",
|
||||
@@ -216,7 +230,7 @@
|
||||
"configureAction": "Einrichten",
|
||||
"syncAction": "Synchronisieren",
|
||||
"showLogsAction": "Zeige Logs",
|
||||
"autocreateUsersOnLogin": "Erstelle User automatisch beim Anmelden auf der Cloudron-Instanz",
|
||||
"autocreateUsersOnLogin": "Erstelle User automatisch beim Anmelden",
|
||||
"auth": "Authentifizierung",
|
||||
"groupnameField": "Gruppennamen Feld",
|
||||
"groupFilter": "Gruppenfilter",
|
||||
@@ -230,11 +244,11 @@
|
||||
"provider": "Anbieter",
|
||||
"noopInfo": "LDAP Authentifizierung ist nicht konfiguriert.",
|
||||
"subscriptionRequiredAction": "Abonnenement jetzt abschließen",
|
||||
"subscriptionRequired": "Diese Funktion ist nur im Abo enthalten.",
|
||||
"description": "Cloudron synchronisiert User und Gruppen aus dem externen LDAP- oder Active-Directory-Server. Passwörter beim Anmelden werden immer durch den externen Server validiert. Die Synchronisierung läuft nicht automatisch, sondern muss manuell gestartet werden.",
|
||||
"description": "Cloudron synchronisiert User und Gruppen aus dem externen LDAP- oder Active-Directory-Server. Die Synchronisierung läuft automatisch, kann aber auch manuell gestartet werden.",
|
||||
"title": "Verbinde ein externes Verzeichnis",
|
||||
"providerOther": "Sonstige",
|
||||
"providerDisabled": "Deaktiviert"
|
||||
"providerDisabled": "Deaktiviert",
|
||||
"disableWarning": "Die Authentifizierungsmethode von allen Usern wird auf die lokale Datenbank zurückgesetzt."
|
||||
},
|
||||
"settings": {
|
||||
"saveAction": "Speichern",
|
||||
@@ -269,8 +283,7 @@
|
||||
"invitationTooltip": "User einladen",
|
||||
"mailmanagerTooltip": "Dieser User kann Benutzer und Postfächer verwalten.",
|
||||
"setGhostTooltip": "Als anderer User ausgeben",
|
||||
"count": "User insgesamt: {{ count }}",
|
||||
"makeLocalTooltip": "Mache user lokal"
|
||||
"count": "User insgesamt: {{ count }}"
|
||||
},
|
||||
"newUserAction": "Neuer User",
|
||||
"role": {
|
||||
@@ -344,7 +357,9 @@
|
||||
"username": "Username",
|
||||
"fullName": "Vollständiger Name",
|
||||
"fallbackEmailPlaceholder": "Optional. Falls nicht gesetzt wird die Primäre E-Mail benutzt",
|
||||
"displayNamePlaceholder": "Optional. Kann während der Registrierung gewählt werden"
|
||||
"displayNamePlaceholder": "Optional. Kann während der Registrierung gewählt werden",
|
||||
"external2FA": "2FA Einstellungen werden von der externen Authentifikationsmethode verwaltet",
|
||||
"ldapGroups": "LDAP Gruppen"
|
||||
},
|
||||
"addUserDialog": {
|
||||
"addUserAction": "User hinzufügen",
|
||||
@@ -388,12 +403,13 @@
|
||||
},
|
||||
"description": "Cloudron kann als zentraler Benutzerverzeichnis-Server für externe Anwendungen fungieren.",
|
||||
"ipRestriction": {
|
||||
"description": "Der Verzeichnisserver kann auf bestimmte IPs oder Bereiche beschränkt werden.",
|
||||
"description": "Der Verzeichnisserver muss auf bestimmte IPs oder Bereiche beschränkt werden. Zeilen, die mit <code>#</code> beginnen werden als Kommentare gewertet.",
|
||||
"label": "Zugriff beschränken",
|
||||
"placeholder": "Zeilen separierte IP Adresse oder Subnetz"
|
||||
},
|
||||
"enabled": "Aktiviert",
|
||||
"title": "Verzeichnis Server"
|
||||
"title": "Verzeichnis Server",
|
||||
"cloudflarePortWarning": "Cloudflare Proxying für die Dashboarddomäne muss deaktiviert sein um den LDAP Server zu erreichen"
|
||||
},
|
||||
"invitationNotification": {
|
||||
"title": "Einladungslink versendet",
|
||||
@@ -421,12 +437,6 @@
|
||||
"all": "Alle User",
|
||||
"active": "Aktive User",
|
||||
"inactive": "Inaktive User"
|
||||
},
|
||||
"makeLocalDialog": {
|
||||
"description": "Dies migriert den User vom externen Verzeichnis zum Cloudron.",
|
||||
"warning": "Das Passwort wird zurückgesetzt um dem User ein lokale Passwort zu geben.",
|
||||
"title": "Mache den Benutzer lokal",
|
||||
"submitAction": "Änderungen lokal speichern"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
@@ -508,7 +518,10 @@
|
||||
"changeEmail": {
|
||||
"errorEmailRequired": "Eine gültige E-Mail-Adresse ist erforderlich",
|
||||
"errorEmailInvalid": "Die E-Mail-Adresse ist nicht gültig",
|
||||
"title": "Primäre E-Mail-Adresse ändern"
|
||||
"title": "Primäre E-Mail-Adresse ändern",
|
||||
"email": "Neue E-Mail-Adresse",
|
||||
"password": "Passwort zur Bestätigung",
|
||||
"errorWrongPassword": "Falsches Passwort"
|
||||
},
|
||||
"loginTokens": {
|
||||
"logoutAll": "Von allen abmelden",
|
||||
@@ -537,14 +550,15 @@
|
||||
},
|
||||
"changeBackgroundImage": {
|
||||
"title": "Hintergrundbild setzen"
|
||||
}
|
||||
},
|
||||
"enable2FANotAvailable": "Für externe User nicht verfügbar"
|
||||
},
|
||||
"emails": {
|
||||
"title": "E-Mail",
|
||||
"settings": {
|
||||
"spamFilter": "Spamfilter",
|
||||
"maxMailSize": "Maximalgröße einer E-Mail",
|
||||
"location": "Standort des Mail-Servers",
|
||||
"location": "Domäne des Mail-Servers",
|
||||
"info": "Die Einstellungen sind global und werden bei allen Domains verwendet.",
|
||||
"title": "Einstellungen",
|
||||
"spamFilterOverview": "{{ blacklistCount }} Adressen sind auf der Blockliste.",
|
||||
@@ -555,7 +569,8 @@
|
||||
"solrNotRunning": "Inaktiv",
|
||||
"solrRunning": "Aktiv",
|
||||
"aclOverview": "{{ dnsblZonesCount }} DNSBL Zonen",
|
||||
"acl": "Postfachberechtigungen"
|
||||
"acl": "Postfachberechtigungen",
|
||||
"virtualAllMail": "\"All Mail\" Ordner"
|
||||
},
|
||||
"domains": {
|
||||
"testEmailTooltip": "Test E-Mail senden",
|
||||
@@ -604,7 +619,7 @@
|
||||
},
|
||||
"changeDomainDialog": {
|
||||
"locationPlaceholder": "Leer lassen, um die Haupt-Domäne zu verwenden",
|
||||
"description": "Cloudron nimmt die notwendigen DNS-Änderungen in allen Domänen vor und startet den Mail-Server neu. Desktop & Mobile E-Mail-Clients müssen neu konfiguriert werden, um diese neue Adresse als IMAP- und SMTP-Server zu verwenden.",
|
||||
"description": "Dies zieht den E-Mail Server auf die neue Domäne um.",
|
||||
"location": "Adresse",
|
||||
"title": "E-Mail-Server Standort ändern",
|
||||
"manualInfo": "Manuell einen A-Eintrag für {{ Domain }} zur öffentlichen IP dieses Cloudrons hinzufügen"
|
||||
@@ -655,6 +670,10 @@
|
||||
},
|
||||
"action": {
|
||||
"queue": "Warteschlange"
|
||||
},
|
||||
"changeVirtualAllMailDialog": {
|
||||
"description": "Der \"All Mail\" Ordner ist ein einziger Ordner, welcher alle E-Mails des Posteingangs beinhaltet. Dieser Ordner unterstützt mit E-Mail Anwendungen, welche keine rekursive Suche anbieten.",
|
||||
"title": "\"All Mail\" Ordner"
|
||||
}
|
||||
},
|
||||
"support": {
|
||||
@@ -677,7 +696,8 @@
|
||||
"report": "Meldung",
|
||||
"subscriptionRequiredDescription": "Antworten auf die häufigsten Fragen sind in der <a href=\"{{ supportViewLink }}\" target=\"_blank\">Dokumentation</a> verfügbar. Unser <a href=\"{{ forumLink }}\" target=\"_blank\">Forum</a> bietet einen Platz in die Community einzusteigen und sich auszutauschen.",
|
||||
"emailVerifyAction": "Jetzt verifizieren",
|
||||
"emailNotVerified": "Ihre cloudron.io Konto E-Mail {{ email }} ist nicht verifiziert. Bitte bestätigen Sie Ihre E-Mail Adresse, um Support-Tickets zu öffnen."
|
||||
"emailNotVerified": "Ihre cloudron.io Konto E-Mail {{ email }} ist nicht verifiziert. Bitte bestätigen Sie Ihre E-Mail Adresse, um Support-Tickets zu öffnen.",
|
||||
"typeBilling": "Problem mit Rechnung"
|
||||
},
|
||||
"remoteSupport": {
|
||||
"title": "Fernwartung",
|
||||
@@ -686,6 +706,10 @@
|
||||
"subscriptionRequired": "Fernwartung ist nur im Abo verfügbar.",
|
||||
"description": "Diese Option aktivieren, um Mitarbeitenden aus dem Support zu erlauben, sich über SSH mit diesem Server zu verbinden.",
|
||||
"disableAction": "Zugang zur SSH-Unterstützung deaktivieren"
|
||||
},
|
||||
"help": {
|
||||
"description": "Bitte die folgenden Resourcen für Hilfe und Support:\n* [Cloudron Forum]({{ forumLink }}) - Bitte die Support und App spezifischen Kategorien nutzen .\n* [Cloudron Doku & Wissensdatenbank]({{ docsLink }})\n* [Custom App Packaging & API]({{ packagingLink }})\n",
|
||||
"title": "Hilfe"
|
||||
}
|
||||
},
|
||||
"eventlog": {
|
||||
@@ -751,12 +775,18 @@
|
||||
"cloudflareDefaultProxyStatus": "Proxying für neue DNS-Einträge aktivieren",
|
||||
"porkbunSecretapikey": "Geheimer API-Schlüssel",
|
||||
"porkbunApikey": "API-Schlüssel",
|
||||
"bunnyAccessKey": "Bunny Access Key"
|
||||
"bunnyAccessKey": "Bunny Access Key",
|
||||
"deSecToken": "deSEC Token",
|
||||
"dnsimpleAccessToken": "Access Token",
|
||||
"ovhEndpoint": "Endpoint",
|
||||
"ovhConsumerKey": "Consumer Key",
|
||||
"ovhAppKey": "Application Key",
|
||||
"ovhAppSecret": "Application Secret"
|
||||
},
|
||||
"changeDashboardDomain": {
|
||||
"title": "Die Dashboard-Domäne ändern",
|
||||
"showLogsAction": "Logfiles anzeigen",
|
||||
"description": "Dadurch werden das Dashboard und der E-Mail-Server in die Subdomain <code>my</code> der ausgewählten Domäne verschoben.",
|
||||
"description": "Dadurch wird das Dashboard in die Subdomain <code>my</code> der ausgewählten Domäne verschoben.",
|
||||
"changeAction": "Domäne ändern",
|
||||
"cancelAction": "Abbrechen"
|
||||
},
|
||||
@@ -784,7 +814,8 @@
|
||||
"tooltipWellKnown": ".well-known Pfade setzen",
|
||||
"domainWellKnown": {
|
||||
"title": ".well-known Pfade von {{ domain }}"
|
||||
}
|
||||
},
|
||||
"count": "Domänenanzahl: {{ count }}"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Benachrichtigungen",
|
||||
@@ -815,7 +846,19 @@
|
||||
"title": "CPU-Auslastung",
|
||||
"graphSubtext": "Es werden nur Anwendungen angezeigt, die mehr als {{ threshold }} an Rechenleistung benötigen"
|
||||
},
|
||||
"selectPeriodLabel": "Zeitraum auswählen"
|
||||
"selectPeriodLabel": "Zeitraum auswählen",
|
||||
"info": {
|
||||
"platformVersion": "Plattform Version",
|
||||
"title": "Info",
|
||||
"vendor": "Anbieter",
|
||||
"product": "Produkt",
|
||||
"memory": "Arbeitsspeicher",
|
||||
"uptime": "Betriebszeit",
|
||||
"activationTime": "Cloudron Aktivierungszeit"
|
||||
},
|
||||
"graphs": {
|
||||
"title": "Graphen"
|
||||
}
|
||||
},
|
||||
"backups": {
|
||||
"title": "Datensicherung",
|
||||
@@ -941,7 +984,8 @@
|
||||
"tooltip": "Dadurch bleiben auch die Mail- und {{ appsLength }} App-Backups erhalten.",
|
||||
"description": "Backup unabhängig von der Aufbewahrungsrichtlinie beibehalten"
|
||||
},
|
||||
"label": "Label"
|
||||
"label": "Label",
|
||||
"remotePath": "Remote Pfad"
|
||||
}
|
||||
},
|
||||
"appstore": {
|
||||
@@ -960,7 +1004,10 @@
|
||||
"email": "E-Mail",
|
||||
"description": "Dieses Konto gibt Zugriff zum App-Store und Aboverwaltung",
|
||||
"titleLogin": "Bei Cloudron.io anmelden",
|
||||
"titleSignUp": "Bei Cloudron.io registrieren"
|
||||
"titleSignUp": "Bei Cloudron.io registrieren",
|
||||
"setupWithTokenAction": "Registrieren",
|
||||
"setupToken": "Setup Token",
|
||||
"titleToken": "Mit Setup Token registrieren"
|
||||
},
|
||||
"appNotFoundDialog": {
|
||||
"description": "Die Anwendung <b>{{ appId }}</b> mit der Version <b>{{ version }}</b> existiert nicht.",
|
||||
@@ -1060,7 +1107,9 @@
|
||||
"title": "Fußzeile"
|
||||
},
|
||||
"logo": "Logo",
|
||||
"cloudronName": "Name der Cloudron-Instanz"
|
||||
"cloudronName": "Name der Cloudron-Instanz",
|
||||
"backgroundImage": "Hintergrundbild der Login-Seite",
|
||||
"clearBackgroundImage": "Löschen"
|
||||
},
|
||||
"login": {
|
||||
"password": "Passwort",
|
||||
@@ -1069,12 +1118,15 @@
|
||||
"2faToken": "2FA-Token (wenn aktiviert)",
|
||||
"loginTo": "Anmeldung bei",
|
||||
"signInAction": "Anmelden",
|
||||
"resetPasswordAction": "Passwort zurücksetzen"
|
||||
"resetPasswordAction": "Passwort zurücksetzen",
|
||||
"loginWith": "Mit Cloudron anmelden",
|
||||
"errorIncorrect2FAToken": "2FA Token ist ungültig",
|
||||
"errorInternal": "Interner Fehler, später nochmals versuchen"
|
||||
},
|
||||
"welcomeEmail": {
|
||||
"welcomeTo": "Willkommen bei <%= cloudronName %>!",
|
||||
"subject": "Willkommen bei <%= cloudron %>",
|
||||
"inviteLinkActionText": "Öffnen den folgenden Link um dich anzumelden: <%- inviteLink %>",
|
||||
"inviteLinkActionText": "Öffne den folgenden Link, um dich anzumelden: <%- inviteLink %>",
|
||||
"expireNote": "Dieser Link ist 7 Tage gültig.",
|
||||
"invitor": "Diese Email wurde geschickt, weil Du von <%= invitor %> eingeladen wurdest.",
|
||||
"inviteLinkAction": "Starte hier",
|
||||
@@ -1178,7 +1230,7 @@
|
||||
"enableEmailDialog": {
|
||||
"description": "Dies wird Cloudron so konfigurieren, dass E-Mails für <b>{{ domain }}</b> empfangen werden. Die Dokumentation zum Öffnen der <a href=\"{{ requiredPortsDocsLink }}\" target=\"_blank\">erforderlichen Ports</a> für Cloudron E-Mail lesen.",
|
||||
"noProviderInfo": "Es ist kein DNS-Anbieter eingerichtet. Die in der Registerkarte Status aufgeführten DNS-Einträge müssen manuell eingerichtet werden.",
|
||||
"cloudflareInfo": "Die Domäne <code>{{ adminDomain }}</code> wird von Cloudflare verwaltet. Sicherstellen, dass das Cloudflare-Proxying für <code>{{ mailFqdn }}</code> deaktiviert und auf <code>DNS only</code> gesetzt ist. Dies ist erforderlich, da Cloudflare kein E-Mail-Proxying durchführt.",
|
||||
"cloudflareInfo": "Die E-Mail Domäne <code>{{ adminDomain }}</code> wird von Cloudflare verwaltet. Sicherstellen, dass das Cloudflare-Proxying für <code>{{ mailFqdn }}</code> deaktiviert und auf <code>DNS only</code> gesetzt ist. Dies ist erforderlich, da Cloudflare kein E-Mail-Proxying durchführt.",
|
||||
"enableAction": "Aktivieren",
|
||||
"title": "E-Mail für {{ domain }} aktivieren?",
|
||||
"setupDnsCheckbox": "DNS-Einträge für E-Mail jetzt einrichten",
|
||||
@@ -1326,7 +1378,8 @@
|
||||
"renameDialog": {
|
||||
"newName": "Neuer Name",
|
||||
"title": "{{ fileName }} umbennen",
|
||||
"rename": "Umbenennen"
|
||||
"rename": "Umbenennen",
|
||||
"reallyOverwrite": "Eine Datei mit diesem Namen existiert bereits. Diese Datei überschreiben?"
|
||||
},
|
||||
"extractDialog": {
|
||||
"title": "Extrahieren von {{ fileName }}",
|
||||
@@ -1388,7 +1441,19 @@
|
||||
},
|
||||
"status": {
|
||||
"restartingApp": "Die Anwendung wird neugestartet"
|
||||
}
|
||||
},
|
||||
"uploader": {
|
||||
"uploading": "Hochladen",
|
||||
"exitWarning": "Aktuell werden noch Dateien hochgeladen. Wirklich schließen?"
|
||||
},
|
||||
"textEditor": {
|
||||
"undo": "Rückgängig",
|
||||
"redo": "Wiederherstellen",
|
||||
"save": "Speichern"
|
||||
},
|
||||
"extractionInProgress": "Entpacken läuft",
|
||||
"pasteInProgress": "Einfügen läuft",
|
||||
"deleteInProgress": "Löschen läuft"
|
||||
},
|
||||
"passwordReset": {
|
||||
"usernameOrEmail": "Username oder E-Mail-Adresse",
|
||||
@@ -1464,14 +1529,14 @@
|
||||
"logsActionTooltip": "Logfiles",
|
||||
"resources": {
|
||||
"cpu": {
|
||||
"setAction": "Festlegen",
|
||||
"title": "CPU-Freigabe",
|
||||
"description": "Prozent der CPU-Zeit, wenn das System unter hoher Last steht."
|
||||
"setAction": "Skalieren",
|
||||
"title": "CPU Limit",
|
||||
"description": "Maximale CPU Prozente, die dieser App zur Verfügung stehen"
|
||||
},
|
||||
"memory": {
|
||||
"resizeAction": "Größe ändern",
|
||||
"title": "Speicherlimit",
|
||||
"description": "Cloudron weist 50% dieses Wertes als RAM und 50% als Swap zu.",
|
||||
"description": "Maximaler Arbeitsspeicher der dieser App zur Verfügung steht",
|
||||
"error": "Speicherlimit nicht einstellbar. Weniger versuchen."
|
||||
}
|
||||
},
|
||||
@@ -1538,7 +1603,7 @@
|
||||
},
|
||||
"uninstall": {
|
||||
"backupWarning": "Anwendungs-Backups werden nicht entfernt und auf der Grundlage der Backup-Richtlinie bereinigt. Diese Anwendung kann aus einem bestehenden App-Backup mit den folgenden <a target=\"_blank\" href=\"{{ importBackupDocsLink }}\">Schritten</a> wiederhergestellt werden.",
|
||||
"description": "Dies wird die Anwendung sofort deinstallieren und alle Daten löschen. Die Anwendung steht anschließend nicht mehr zur Verfügung.",
|
||||
"description": "Dies wird die Anwendung sofort deinstallieren und alle zugehörigen Daten löschen. Die Anwendung steht anschließend nicht mehr zur Verfügung.",
|
||||
"title": "Deinstallieren",
|
||||
"uninstallAction": "Deinstallieren"
|
||||
}
|
||||
@@ -1554,12 +1619,12 @@
|
||||
},
|
||||
"updates": {
|
||||
"auto": {
|
||||
"enableAction": "Automatische Aktualisierungen aktivieren",
|
||||
"enableAction": "Aktivieren",
|
||||
"disabled": "Die automatische Aktualisierung ist deaktiviert.",
|
||||
"enabled": "Die automatische Aktualisierung ist aktiviert.",
|
||||
"title": "Automatische Aktualisierungen",
|
||||
"description": "Cloudron fragt regelmäßig den App-Store nach Aktualisierungen ab. Wenn automatisches Aktualisieren deaktiviert ist, bitte sicherstellen, dass manuell nach Aktualisierungen gesucht wird.",
|
||||
"disableAction": "Automatische Aktualisierungen deaktivieren"
|
||||
"disableAction": "Deaktivieren"
|
||||
},
|
||||
"info": {
|
||||
"updateAvailableAction": "Aktualisierung verfügbar",
|
||||
@@ -1570,7 +1635,8 @@
|
||||
"customAppUpdateInfo": "Aktualiserung steht für benutzerdefinierte Anwendungen nicht zur Verfügung",
|
||||
"checkForUpdatesAction": "Auf Aktualisierungen überprüfen",
|
||||
"packageVersion": "Paket-Version",
|
||||
"repository": "Paket-Repository"
|
||||
"repository": "Paket-Repository",
|
||||
"installedAt": "Installationszeitpunkt"
|
||||
},
|
||||
"noUpdates": "Keine neuen Updates verfügbar"
|
||||
},
|
||||
@@ -1617,7 +1683,8 @@
|
||||
"dataDirPlaceholder": "Leer lassen, um Systemvorgabe zu verwenden",
|
||||
"description": "Wenn dem Server der Speicherplatz ausgeht, kann durch Hinzufügen einer <a href=\"/#/volumes\">externen Festplatte</a>, die Daten der Anwendung dorthin verschoben werden.",
|
||||
"moveAction": "Daten verschieben",
|
||||
"diskUsage": "Die App verwendet derzeit {{ size }} an Speicherplatz (ab {{ date }})."
|
||||
"diskUsage": "Die App verwendet derzeit {{ size }} an Speicherplatz (ab {{ date }}).",
|
||||
"mountTypeWarning": "Das Zieldateisystem muss Dateiberechtigungen und Eigentümerschaft unterstützen, damit die Verschiebung funktioniert"
|
||||
},
|
||||
"mounts": {
|
||||
"title": "Mounts",
|
||||
@@ -1767,12 +1834,31 @@
|
||||
},
|
||||
"addApplinkDialog": {
|
||||
"title": "Link zur externen Anwendung hinzufügen"
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"disable": "Redis deaktivieren",
|
||||
"title": "Redis Konfiguration",
|
||||
"enable": "Die App mit Redis vorkonfigurieren"
|
||||
},
|
||||
"infoTabTitle": "Info",
|
||||
"info": {
|
||||
"notes": {
|
||||
"title": "Administrator Notizen"
|
||||
}
|
||||
},
|
||||
"turn": {
|
||||
"enable": "App für den internen TURN Server konfigurieren",
|
||||
"disable": "TURN Server dieser App nicht automatisch konfigurieren.",
|
||||
"title": "TURN Einstellungen"
|
||||
},
|
||||
"servicesTabTitle": "Dienste"
|
||||
},
|
||||
"logs": {
|
||||
"download": "Vollständige Logfiles herunterladen",
|
||||
"title": "Logfiles",
|
||||
"clear": "Anzeige löschen"
|
||||
"clear": "Anzeige löschen",
|
||||
"notFoundError": "Task oder App existiert nicht",
|
||||
"logsGoneError": "Logdatei(n) nicht gefunden"
|
||||
},
|
||||
"lang": {
|
||||
"en": "Englisch",
|
||||
@@ -1786,7 +1872,8 @@
|
||||
"es": "Spanisch",
|
||||
"ru": "Russisch",
|
||||
"pt": "Portugiesisch",
|
||||
"da": "Dänisch"
|
||||
"da": "Dänisch",
|
||||
"id": "Indonesian"
|
||||
},
|
||||
"volumes": {
|
||||
"description": "Datenträger sind Verzeichnisse auf dem Server, die von Anwendungen gemeinsam genutzt werden können.",
|
||||
@@ -1823,7 +1910,11 @@
|
||||
"mountStatus": "Einhängestatus",
|
||||
"localDirectory": "Lokales Verzeichnis",
|
||||
"type": "Typ",
|
||||
"remountActionTooltip": "Datenträger neu einhängen"
|
||||
"remountActionTooltip": "Datenträger neu einhängen",
|
||||
"editVolumeDialog": {
|
||||
"title": "Datenträger {{ name }} konfigurieren"
|
||||
},
|
||||
"editActionTooltip": "Datenträger konfigurieren"
|
||||
},
|
||||
"lang.ja": "Japanisch",
|
||||
"newLoginEmail": {
|
||||
@@ -1877,5 +1968,6 @@
|
||||
"newClient": "Neuer Client",
|
||||
"empty": "Noch keine Clienten erstellt"
|
||||
}
|
||||
}
|
||||
},
|
||||
"automation": "Automatisierung"
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
"addApplinkAction": "Add App Link",
|
||||
"filter": {
|
||||
"clearAll": "Clear All"
|
||||
},
|
||||
"apps": {
|
||||
"count": "Total apps: {{ count }}"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
@@ -81,7 +84,8 @@
|
||||
"justNow": "just now",
|
||||
"yeserday": "Yesterday",
|
||||
"minutesAgo": "{{ m }} minutes ago",
|
||||
"hoursAgo": "{{ h }} hours ago"
|
||||
"hoursAgo": "{{ h }} hours ago",
|
||||
"never": "Never"
|
||||
},
|
||||
"navbar": {
|
||||
"users": "Users"
|
||||
@@ -170,7 +174,10 @@
|
||||
"loginAction": "Login",
|
||||
"createAccountAction": "Create Account",
|
||||
"switchToSignUpAction": "Don't have an account yet? Sign up",
|
||||
"switchToLoginAction": "Already have an account? Log in"
|
||||
"switchToLoginAction": "Already have an account? Log in",
|
||||
"setupWithTokenAction": "Setup",
|
||||
"setupToken": "Setup Token",
|
||||
"titleToken": "Sign up with Setup Token"
|
||||
},
|
||||
"categoryLabel": "Category",
|
||||
"ssofilter": {
|
||||
@@ -198,8 +205,7 @@
|
||||
"invitationTooltip": "Invite User",
|
||||
"setGhostTooltip": "Impersonate",
|
||||
"mailmanagerTooltip": "This user can manage users and mailboxes",
|
||||
"count": "Total users: {{ count }}",
|
||||
"makeLocalTooltip": "Make user local"
|
||||
"count": "Total users: {{ count }}"
|
||||
},
|
||||
"groups": {
|
||||
"title": "Groups",
|
||||
@@ -219,8 +225,7 @@
|
||||
},
|
||||
"externalLdap": {
|
||||
"title": "Connect an External Directory",
|
||||
"description": "Cloudron will synchronize users and groups from an external LDAP or ActiveDirectory server. Password verification for authenticating those users is done against the external server. The synchronization is not run automatically but needs to be triggered manually.",
|
||||
"subscriptionRequired": "This feature is only available in the paid plans.",
|
||||
"description": "This setting will synchronize and authenticate users and groups from an external LDAP or Active Directory server. The synchronization is run periodically but can also be triggered manually.",
|
||||
"subscriptionRequiredAction": "Set up Subscription Now",
|
||||
"noopInfo": "LDAP authentication is not configured.",
|
||||
"provider": "Provider",
|
||||
@@ -234,15 +239,16 @@
|
||||
"groupFilter": "Group Filter",
|
||||
"groupnameField": "Groupname Field",
|
||||
"auth": "Auth",
|
||||
"autocreateUsersOnLogin": "Automatically create users when they login to Cloudron",
|
||||
"autocreateUsersOnLogin": "Automatically create users on login",
|
||||
"showLogsAction": "Show Logs",
|
||||
"syncAction": "Synchronize",
|
||||
"syncAction": "Sync",
|
||||
"configureAction": "Configure",
|
||||
"bindUsername": "Bind DN/Username (optional)",
|
||||
"bindPassword": "Bind Password (optional)",
|
||||
"errorSelfSignedCert": "Server is using an invalid or self-signed certificate.",
|
||||
"providerOther": "Other",
|
||||
"providerDisabled": "Disabled"
|
||||
"providerDisabled": "Disabled",
|
||||
"disableWarning": "The authentication source of all existing users will be reset to authenticate against the local password database."
|
||||
},
|
||||
"subscriptionDialog": {
|
||||
"title": "Subscription required",
|
||||
@@ -271,7 +277,9 @@
|
||||
"errorDisplayNameRequired": "Name is required",
|
||||
"activeCheckbox": "User is active",
|
||||
"displayNamePlaceholder": "Optional. If not provided, user can provide during sign up",
|
||||
"fallbackEmailPlaceholder": "Optional. If not specified, primary email will be used"
|
||||
"fallbackEmailPlaceholder": "Optional. If not specified, primary email will be used",
|
||||
"external2FA": "2FA setup is managed by external authentication source",
|
||||
"ldapGroups": "LDAP Groups"
|
||||
},
|
||||
"deleteUserDialog": {
|
||||
"title": "Delete user {{ username }}",
|
||||
@@ -360,7 +368,7 @@
|
||||
"description": "Cloudron can act as a central user directory server for external applications.",
|
||||
"enabled": "Enabled",
|
||||
"ipRestriction": {
|
||||
"description": "The directory server can be limited to specific IPs or ranges.",
|
||||
"description": "Limit Directory Server access to specific IPs or ranges. Lines starting with <code>#</code> are treated as comments.",
|
||||
"placeholder": "Line separated IP address or Subnet",
|
||||
"label": "Restrict Access"
|
||||
},
|
||||
@@ -368,7 +376,8 @@
|
||||
"label": "Bind Password",
|
||||
"description": "All LDAP queries have to be authenticated with this secret and the user DN <i>{{ userDN }}</i>",
|
||||
"url": "Server URL"
|
||||
}
|
||||
},
|
||||
"cloudflarePortWarning": "Cloudflare proxying must be disabled on the dashboard domain to access the LDAP server"
|
||||
},
|
||||
"userImportDialog": {
|
||||
"title": "Import Users",
|
||||
@@ -392,12 +401,6 @@
|
||||
"all": "All Users",
|
||||
"active": "Active Users",
|
||||
"inactive": "Inactive Users"
|
||||
},
|
||||
"makeLocalDialog": {
|
||||
"title": "Make this user local",
|
||||
"description": "This will migrate the user from the external directory to the Cloudron.",
|
||||
"warning": "A password reset will be initiated to set a local password for this user.",
|
||||
"submitAction": "Make local"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
@@ -464,7 +467,10 @@
|
||||
"changeEmail": {
|
||||
"title": "Change primary email address",
|
||||
"errorEmailInvalid": "The Email address is not valid",
|
||||
"errorEmailRequired": "A valid email address is required"
|
||||
"errorEmailRequired": "A valid email address is required",
|
||||
"email": "New Email Address",
|
||||
"password": "Password for confirmation",
|
||||
"errorWrongPassword": "Wrong password"
|
||||
},
|
||||
"changeFallbackEmail": {
|
||||
"title": "Change password recovery email address",
|
||||
@@ -627,7 +633,7 @@
|
||||
},
|
||||
"check": {
|
||||
"noop": "Cloudron backups are disabled. Please ensure this server is backed up using alternate means. See https://docs.cloudron.io/backups/#storage-providers for more information.",
|
||||
"sameDisk": "Cloudron backups are currently on the same disk as the Cloudron server instance. This is dangerous and can lead to complete data loss if the disk fails. See https://docs.cloudron.io/backups/#storage-providers for storing backups in an external location."
|
||||
"sameDisk": "Backups are currently on the same disk as Cloudron itself. If the disk fills up with these backups, Cloudron will not function. A disk failure can also lead to complete data loss. See https://docs.cloudron.io/backups/#storage-providers for storing backups in an external location."
|
||||
},
|
||||
"backupEdit": {
|
||||
"title": "Edit Backup",
|
||||
@@ -651,7 +657,9 @@
|
||||
},
|
||||
"changeLogo": {
|
||||
"title": "Choose Cloudron Avatar"
|
||||
}
|
||||
},
|
||||
"backgroundImage": "Login page background image",
|
||||
"clearBackgroundImage": "Clear"
|
||||
},
|
||||
"emails": {
|
||||
"title": "Email",
|
||||
@@ -779,8 +787,8 @@
|
||||
"network": {
|
||||
"title": "Network",
|
||||
"ip": {
|
||||
"title": "IP Address",
|
||||
"description": "Cloudron uses this IP address when setting up DNS records.",
|
||||
"title": "IPv4",
|
||||
"description": "Cloudron uses this IPv4 address to setup DNS A records.",
|
||||
"provider": "Provider",
|
||||
"interface": "Network Interface Name",
|
||||
"configure": "Configure",
|
||||
@@ -804,7 +812,7 @@
|
||||
"showLogsAction": "Show Logs"
|
||||
},
|
||||
"configureIp": {
|
||||
"title": "Configure IP Provider",
|
||||
"title": "Configure IPv4 Provider",
|
||||
"providerGenericDescription": "The Public IP address of the server will be automatically detected."
|
||||
},
|
||||
"ipv4": {
|
||||
@@ -856,7 +864,7 @@
|
||||
"cloudronId": "Cloudron ID",
|
||||
"subscriptionEndsAt": "Canceled and ends on",
|
||||
"subscriptionSetupAction": "Upgrade to Premium",
|
||||
"subscriptionChangeAction": "Change Subscription",
|
||||
"subscriptionChangeAction": "Manage Subscription",
|
||||
"subscriptionReactivateAction": "Reactivate Subscription",
|
||||
"emailNotVerified": "Email not yet verified"
|
||||
},
|
||||
@@ -949,6 +957,10 @@
|
||||
"warning": "Do not enable this option unless requested by the Cloudron support team.",
|
||||
"disableAction": "Disable SSH support access",
|
||||
"enableAction": "Enable SSH support access"
|
||||
},
|
||||
"help": {
|
||||
"title": "Help",
|
||||
"description": "Please use the following resources for help and support:\n* [Cloudron Forum]({{ forumLink }}) - Please use the Support and App specific categories for questions.\n* [Cloudron Docs & Knowledge Base]({{ docsLink }})\n* [Custom App Packaging & API]({{ packagingLink }})\n"
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
@@ -973,7 +985,19 @@
|
||||
"graphTitle": "Percentage",
|
||||
"graphSubtext": "Only apps using more than {{ threshold }} of cpu are shown"
|
||||
},
|
||||
"selectPeriodLabel": "Select Period"
|
||||
"selectPeriodLabel": "Select Period",
|
||||
"info": {
|
||||
"platformVersion": "Platform Version",
|
||||
"title": "Info",
|
||||
"vendor": "Vendor",
|
||||
"product": "Product",
|
||||
"memory": "Memory",
|
||||
"uptime": "Uptime",
|
||||
"activationTime": "Cloudron Creation Time"
|
||||
},
|
||||
"graphs": {
|
||||
"title": "Graphs"
|
||||
}
|
||||
},
|
||||
"eventlog": {
|
||||
"title": "Event Log",
|
||||
@@ -1055,7 +1079,13 @@
|
||||
"cloudflareDefaultProxyStatus": "Enable proxying for new DNS records",
|
||||
"porkbunApikey": "API Key",
|
||||
"porkbunSecretapikey": "Secret API Key",
|
||||
"bunnyAccessKey": "Bunny Access Key"
|
||||
"bunnyAccessKey": "Bunny Access Key",
|
||||
"dnsimpleAccessToken": "Access Token",
|
||||
"ovhEndpoint": "Endpoint",
|
||||
"ovhConsumerKey": "Consumer Key",
|
||||
"ovhAppKey": "Application Key",
|
||||
"ovhAppSecret": "Application Secret",
|
||||
"deSecToken": "deSEC Token"
|
||||
},
|
||||
"removeDialog": {
|
||||
"title": "Really remove {{ domain }}?",
|
||||
@@ -1113,7 +1143,8 @@
|
||||
"copy": "Copy",
|
||||
"clear": "Clear",
|
||||
"pasteInfo": "For Paste use Ctrl+v"
|
||||
}
|
||||
},
|
||||
"uploadTo": "Upload to {{ path }}"
|
||||
},
|
||||
"filemanager": {
|
||||
"title": "File Manager",
|
||||
@@ -1131,7 +1162,8 @@
|
||||
"renameDialog": {
|
||||
"title": "Rename {{ fileName }}",
|
||||
"newName": "New Name",
|
||||
"rename": "Rename"
|
||||
"rename": "Rename",
|
||||
"reallyOverwrite": "A file with that name already exists. Overwrite existing file?"
|
||||
},
|
||||
"chownDialog": {
|
||||
"title": "Change ownership",
|
||||
@@ -1334,7 +1366,7 @@
|
||||
"title": "Enable Email for {{ domain }}?",
|
||||
"description": "This will configure Cloudron to receive emails for <b>{{ domain }}</b>. See the documentation for opening up the <a href=\"{{ requiredPortsDocsLink }}\" target=\"_blank\">required ports</a> for Cloudron Email.",
|
||||
"noProviderInfo": "No DNS provider is set up. The DNS records listed in the Status tab have to be set up manually.",
|
||||
"cloudflareInfo": "The domain <code>{{ adminDomain }}</code> is managed by Cloudflare. Please verify that Cloudflare proxying is disabled for <code>{{ mailFqdn }}</code> and set to <code>DNS only</code>. This is required because Cloudflare does not proxy email.",
|
||||
"cloudflareInfo": "The mail server's domain <code>{{ adminDomain }}</code> is managed by Cloudflare. Please verify that Cloudflare proxying is disabled for <code>{{ mailFqdn }}</code> and set to <code>DNS only</code>. This is required because Cloudflare does not proxy email.",
|
||||
"setupDnsCheckbox": "Set up Mail DNS records now",
|
||||
"setupDnsInfo": "Use this option to automatically set up Email related DNS records. Leaving this option unchecked is useful for creating mail boxes and <a href=\"{{ importEmailDocsLink }}\">importing email</a> before going live.",
|
||||
"enableAction": "Enable"
|
||||
@@ -1470,14 +1502,14 @@
|
||||
"resources": {
|
||||
"memory": {
|
||||
"title": "Memory Limit",
|
||||
"description": "Cloudron allocates 50% of this value as RAM and 50% as swap.",
|
||||
"description": "Maximum memory app can use",
|
||||
"error": "Unable to set memory limit, try less.",
|
||||
"resizeAction": "Resize"
|
||||
},
|
||||
"cpu": {
|
||||
"setAction": "Set",
|
||||
"title": "CPU Shares",
|
||||
"description": "Percent of CPU time when system is under heavy load."
|
||||
"setAction": "Scale",
|
||||
"title": "CPU Limit",
|
||||
"description": "Maximum percent of CPU app can use"
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
@@ -1561,17 +1593,18 @@
|
||||
"packageVersion": "Package Version",
|
||||
"lastUpdated": "Last Updated",
|
||||
"checkForUpdatesAction": "Check for Updates",
|
||||
"customAppUpdateInfo": "Updates are not available for custom apps",
|
||||
"customAppUpdateInfo": "Auto-update is not available for custom apps.",
|
||||
"updateAvailableAction": "Update Available",
|
||||
"repository": "Package Repository"
|
||||
"repository": "Package Repository",
|
||||
"installedAt": "Installed At"
|
||||
},
|
||||
"auto": {
|
||||
"title": "Automatic Updates",
|
||||
"description": "Cloudron periodically checks the App Store for updates. If you disable automatic updates, be sure to manually apply the updates.",
|
||||
"description": "Cloudron periodically checks the <a href=\"{{ appStoreLink }}\" target=\"_blank\">App Store</a> for updates.",
|
||||
"enabled": "Automatic Updates is currently enabled.",
|
||||
"disabled": "Automatic Updates is currently disabled.",
|
||||
"disableAction": "Disable Automatic Updates",
|
||||
"enableAction": "Enable Automatic Updates"
|
||||
"disableAction": "Disable Auto-update",
|
||||
"enableAction": "Enable Auto-update"
|
||||
},
|
||||
"noUpdates": "No new updates available"
|
||||
},
|
||||
@@ -1625,7 +1658,7 @@
|
||||
},
|
||||
"uninstall": {
|
||||
"title": "Uninstall",
|
||||
"description": "This will uninstall the app immediately and remove all its data. The site will be inaccessible.",
|
||||
"description": "This will uninstall the app immediately and remove the app's data. The site will be inaccessible.",
|
||||
"backupWarning": "App backups are not removed and will be cleaned up based on the backup policy. You can resurrect this app from an existing app backup using the following <a target=\"_blank\" href=\"{{ importBackupDocsLink }}\">instructions</a>.",
|
||||
"uninstallAction": "Uninstall"
|
||||
}
|
||||
@@ -1742,6 +1775,12 @@
|
||||
"title": "Redis Configuration",
|
||||
"enable": "Configure the app to use Redis",
|
||||
"disable": "Disable Redis"
|
||||
},
|
||||
"infoTabTitle": "Info",
|
||||
"info": {
|
||||
"notes": {
|
||||
"title": "Admin Notes"
|
||||
}
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
@@ -1753,7 +1792,8 @@
|
||||
"signInAction": "Sign in",
|
||||
"resetPasswordAction": "Reset password",
|
||||
"errorIncorrect2FAToken": "2FA token is invalid",
|
||||
"errorInternal": "Internal error, try again later"
|
||||
"errorInternal": "Internal error, try again later",
|
||||
"loginWith": "Login with Cloudron"
|
||||
},
|
||||
"passwordReset": {
|
||||
"title": "Password reset",
|
||||
@@ -1834,7 +1874,8 @@
|
||||
"es": "Spanish",
|
||||
"ru": "Russian",
|
||||
"pt": "Portuguese",
|
||||
"da": "Danish"
|
||||
"da": "Danish",
|
||||
"id": "Indonesian"
|
||||
},
|
||||
"volumes": {
|
||||
"title": "Volumes",
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"switchToLoginAction": "¿Ya tienes una cuenta? Inicia sesión",
|
||||
"switchToSignUpAction": "¿No tienes una cuenta todavía? Regístrate",
|
||||
"createAccountAction": "Crear Cuenta",
|
||||
"loginAction": "Iniciar sesión",
|
||||
"loginAction": "Iniciar Sesión",
|
||||
"errorWrongPassword": "Contraseña errónea",
|
||||
"licenseCheckbox": "Acepto la <a href=\"{{ licenseLink }}\" target=\"_blank\">licencia de Cloudron</a>",
|
||||
"chooseAnOption": "Por favor escoge una opción…",
|
||||
@@ -72,7 +72,10 @@
|
||||
"email": "Email",
|
||||
"description": "Esta cuenta se usa para acceder a la App Store y administrar tu suscripción",
|
||||
"titleLogin": "Iniciar sesión en Cloudron.io",
|
||||
"titleSignUp": "Regístrate en Cloudron.io"
|
||||
"titleSignUp": "Regístrate en Cloudron.io",
|
||||
"setupWithTokenAction": "Ajustes",
|
||||
"setupToken": "Configurar Token",
|
||||
"titleToken": "Registrarse con el token de configuración"
|
||||
},
|
||||
"appNotFoundDialog": {
|
||||
"description": "No hay aplicación <b>{{ appId }}</b> con versión <b>{{ version }}</b>.",
|
||||
@@ -97,7 +100,8 @@
|
||||
},
|
||||
"action": {
|
||||
"logs": "Registros",
|
||||
"reboot": "Reiniciar"
|
||||
"reboot": "Reiniciar",
|
||||
"showLogs": "Mostrar registros"
|
||||
},
|
||||
"pagination": {
|
||||
"perPageSelector": "Mostrar {{ n }} por página",
|
||||
@@ -141,7 +145,8 @@
|
||||
"statusEnabled": "Habilitado",
|
||||
"statusDisabled": "Deshabilitado",
|
||||
"loadingPlaceholder": "Cargando",
|
||||
"settings": "Ajustes"
|
||||
"settings": "Ajustes",
|
||||
"saveAction": "Guardar"
|
||||
},
|
||||
"apps": {
|
||||
"domainsFilterHeader": "Todos los Dominios",
|
||||
@@ -161,12 +166,13 @@
|
||||
"description": "¿Qué te parece si instalas algunas? Echa un vistazo a la <a href=\"{{ appStoreLink }}\"> Tienda de Aplicaciones</a>",
|
||||
"title": "¡No hay aplicaciones instaladas todavía!"
|
||||
},
|
||||
"title": "Mis aplicaciones",
|
||||
"title": "Mis Aplicaciones",
|
||||
"groupsFilterHeader": "Todos los Grupos",
|
||||
"auth": {
|
||||
"nosso": "Inicia sesión con una cuenta dedicada",
|
||||
"sso": "Inicia sesión con las credenciales de Cloudron",
|
||||
"email": "Inicia sesión con el correo electrónico"
|
||||
"email": "Inicia sesión con el correo electrónico",
|
||||
"openid": "Iniciar sesión con Cloudron OpenID"
|
||||
},
|
||||
"addAppAction": "Añadir Aplicación",
|
||||
"addAppproxyAction": "Añadir Proxi de la Aplicación",
|
||||
@@ -205,7 +211,6 @@
|
||||
"provider": "Proveedor",
|
||||
"noopInfo": "La autentificación LDAP no está configurada.",
|
||||
"subscriptionRequiredAction": "Configura tu Suscripción Ahora",
|
||||
"subscriptionRequired": "Esta característica solo está habilitada en planes de pago.",
|
||||
"description": "Cloudron sincronizará usuarios y grupos desde un servidor LDAP o ActiveDirectory externo. La verificación de la contraseña para autentificar a esos usuarios se realiza en el servidor externo. La sincronización no se ejecuta automáticamente, sino que debe activarse manualmente.",
|
||||
"title": "Conectar un directorio externo",
|
||||
"auth": "Auth",
|
||||
@@ -218,7 +223,7 @@
|
||||
"subscriptionRequired": "Estas características solo están habilitadas para planes de pago.",
|
||||
"require2FACheckbox": "Requerir que los usuarios configuren 2FA",
|
||||
"allowProfileEditCheckbox": "Permitir a los usuarios editar su nombre y correo",
|
||||
"title": "Ajustes",
|
||||
"title": "Ajustes de usuario",
|
||||
"require2FAWarning": "Configura primero 2FA para tu cuenta para evitar que la bloqueen."
|
||||
},
|
||||
"groups": {
|
||||
@@ -245,7 +250,6 @@
|
||||
"setGhostTooltip": "Suplantar",
|
||||
"invitationTooltip": "Invitar Usuario",
|
||||
"mailmanagerTooltip": "Este usuario puede administrar usuarios y buzones de correo",
|
||||
"makeLocalTooltip": "Hacer que el usuario sea local",
|
||||
"count": "Total usuarios: {{ count }}"
|
||||
},
|
||||
"newUserAction": "Nuevo Usuario",
|
||||
@@ -389,12 +393,6 @@
|
||||
"all": "Todos los Usuarios",
|
||||
"active": "Usuarios Activos",
|
||||
"inactive": "Usuarios Inactivos"
|
||||
},
|
||||
"makeLocalDialog": {
|
||||
"title": "Hacer este usuario local",
|
||||
"description": "Esto migrará el usuario desde un directorio externo a Cloudron.",
|
||||
"submitAction": "Hacer local",
|
||||
"warning": "Se iniciará un restablecimiento de contraseña para establecer una contraseña local para este usuario."
|
||||
}
|
||||
},
|
||||
"backups": {
|
||||
@@ -521,7 +519,8 @@
|
||||
"preserved": {
|
||||
"description": "Copia de seguridad persistente independientemente de la política de retención",
|
||||
"tooltip": "Esto también conservará el correo y las copias de seguridad de la aplicación {{ appsLength }}."
|
||||
}
|
||||
},
|
||||
"remotePath": "Ruta remota"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
@@ -611,7 +610,7 @@
|
||||
"errorPasswordsDontMatch": "Las contraseñas no coinciden",
|
||||
"errorPasswordRequired": "Se requiere una contraseña",
|
||||
"newPasswordRepeat": "Repite nueva contraseña",
|
||||
"newPassword": "Nueva contraseña",
|
||||
"newPassword": "Nueva Contraseña",
|
||||
"currentPassword": "Contraseña actual",
|
||||
"title": "Cambia tu contraseña"
|
||||
},
|
||||
@@ -632,7 +631,8 @@
|
||||
},
|
||||
"changeBackgroundImage": {
|
||||
"title": "Establecer imagen de fondo"
|
||||
}
|
||||
},
|
||||
"enable2FANotAvailable": "No disponible para usuarios de una fuente de autentificación externa"
|
||||
},
|
||||
"emails": {
|
||||
"eventlog": {
|
||||
@@ -679,7 +679,8 @@
|
||||
"info": "Esta configuración es global y se aplica a todos los dominios.",
|
||||
"title": "Ajustes",
|
||||
"acl": "Correo ACL",
|
||||
"aclOverview": "{{ dnsblZonesCount }} zona(s) DNSBL"
|
||||
"aclOverview": "{{ dnsblZonesCount }} zona(s) DNSBL",
|
||||
"virtualAllMail": "Carpeta \"Todos los correos\""
|
||||
},
|
||||
"domains": {
|
||||
"testEmailTooltip": "Enviar Email de prueba",
|
||||
@@ -722,7 +723,7 @@
|
||||
"manualInfo": "Agrega un registro A manualmente para el {{dominio}} a la IP pública de este Cloudron",
|
||||
"locationPlaceholder": "Dejar vacío para usar el dominio desnudo",
|
||||
"location": "Ubicación",
|
||||
"description": "Cloudron realizará los cambios de DNS necesarios en todos los dominios y reiniciará el servidor de correo. Los clientes de correo electrónico de escritorio y móviles deben reconfigurarse para usar esta nueva ubicación como servidor IMAP y SMTP.",
|
||||
"description": "Esto moverá el servidor IMAP y SMTP a la ubicación especificada.",
|
||||
"title": "Cambiar ubicación del Servidor de Correo"
|
||||
},
|
||||
"aclDialog": {
|
||||
@@ -750,6 +751,10 @@
|
||||
},
|
||||
"action": {
|
||||
"queue": "Cola"
|
||||
},
|
||||
"changeVirtualAllMailDialog": {
|
||||
"title": "Carpeta \"Todos los correos\"",
|
||||
"description": "La carpeta \"Todos los correos\" es una carpeta única que contiene todos los correos electrónicos de su bandeja de entrada. La carpeta puede resultar útil en clientes de correo que no admiten la búsqueda recursiva de carpetas."
|
||||
}
|
||||
},
|
||||
"branding": {
|
||||
@@ -794,7 +799,8 @@
|
||||
},
|
||||
"dyndns": {
|
||||
"description": "Habilite esta opción para mantener todos sus registros DNS sincronizados con una dirección IP cambiante. Esto es útil cuando Cloudron se ejecuta en una red con una dirección IP pública que cambia con frecuencia, como una conexión doméstica.",
|
||||
"title": "DNS Dinámico"
|
||||
"title": "DNS Dinámico",
|
||||
"showLogsAction": "Mostrar registros"
|
||||
},
|
||||
"ipv4": {
|
||||
"address": "Dirección IPv4"
|
||||
@@ -806,7 +812,13 @@
|
||||
},
|
||||
"configureIpv6": {
|
||||
"title": "Configurar Proveedor de IPv6"
|
||||
}
|
||||
},
|
||||
"trustedIps": {
|
||||
"summary": "{{ trustCount }} IPs confiables",
|
||||
"description": "Se confiará en los encabezados HTTP de direcciones IP coincidentes",
|
||||
"title": "Configurar IP confiables"
|
||||
},
|
||||
"trustedIpRanges": "Rangos e IPs confiables "
|
||||
},
|
||||
"services": {
|
||||
"configure": {
|
||||
@@ -826,7 +838,7 @@
|
||||
"service": "Servicio",
|
||||
"description": "Los servicios de Cloudron implementan funcionalidades como bases de datos, correo electrónico y autentificación.",
|
||||
"title": "Servicios",
|
||||
"refresh": "Actualizar"
|
||||
"refresh": "Refrescar"
|
||||
},
|
||||
"settings": {
|
||||
"appstoreAccount": {
|
||||
@@ -905,7 +917,7 @@
|
||||
"domains": {
|
||||
"title": "Dominios y Certificados",
|
||||
"changeDashboardDomain": {
|
||||
"description": "Esto moverá el Panel y el Servidor de Correo al subdominio <code>my</code> del dominio seleccionado.",
|
||||
"description": "Esto moverá el panel al subdominio <code>my</code> del dominio seleccionado.",
|
||||
"showLogsAction": "Mostrar Registros",
|
||||
"cancelAction": "Cancelar",
|
||||
"changeAction": "Cambiar Dominio",
|
||||
@@ -958,7 +970,12 @@
|
||||
"bunnyAccessKey": "Clave de acceso Bunny",
|
||||
"cloudflareDefaultProxyStatus": "Habilitar proxy para nuevos registros DNS",
|
||||
"porkbunApikey": "Clave API",
|
||||
"porkbunSecretapikey": "Clave API secreta"
|
||||
"porkbunSecretapikey": "Clave API secreta",
|
||||
"dnsimpleAccessToken": "Token de acceso",
|
||||
"ovhEndpoint": "Punto final",
|
||||
"ovhConsumerKey": "Clave del consumidor",
|
||||
"ovhAppKey": "Clave de Aplicación",
|
||||
"ovhAppSecret": "Clave Secreta Aplicación"
|
||||
},
|
||||
"subscriptionRequired": {
|
||||
"setupAction": "Configura tu suscripción",
|
||||
@@ -1047,7 +1064,8 @@
|
||||
"dataDirPlaceholder": "Dejar vacío para usar la plataforma predeterminada",
|
||||
"description": "Si el servidor se está quedando sin espacio en disco, usa esto para mover los datos de la aplicación a un <a href=\"/#/volumes\">volumen</a>. Cualquier dato aquí es parte de la copia de seguridad de la aplicación.",
|
||||
"moveAction": "Mover datos",
|
||||
"diskUsage": "Actualmente, la aplicación está usando {{ size }} de almacenamiento (hasta el {{ date }})."
|
||||
"diskUsage": "Actualmente, la aplicación está usando {{ size }} de almacenamiento (hasta el {{ date }}).",
|
||||
"mountTypeWarning": "El sistema de archivos de destino debe admitir permisos y propiedad de los archivos para que el traslado funcione"
|
||||
}
|
||||
},
|
||||
"logsActionTooltip": "Registros",
|
||||
@@ -1321,6 +1339,17 @@
|
||||
"label": "Etiqueta",
|
||||
"clearIconAction": "Borrar icono",
|
||||
"clearIconDescription": "Esto intentará obtener el favicon de la aplicación al guardar."
|
||||
},
|
||||
"servicesTabTitle": "Servicios",
|
||||
"turn": {
|
||||
"title": "Configuración de TURN",
|
||||
"enable": "Configura la aplicación para utilizar el servidor TURN integrado",
|
||||
"disable": "No configures los ajustes de la aplicación TURN. Su configuración se deja como está. Puedes hacer los ajustes dentro de la aplicación."
|
||||
},
|
||||
"redis": {
|
||||
"title": "Configuración de Redis",
|
||||
"enable": "Configura la aplicación para usar Redis",
|
||||
"disable": "Deshabilitar Redis"
|
||||
}
|
||||
},
|
||||
"lang": {
|
||||
@@ -1360,7 +1389,19 @@
|
||||
"volumeContent": "Este disco es el volumen <code>{{ name }}</code>",
|
||||
"diskSpeed": "Velocidad: {{ speed }} MB/seg"
|
||||
},
|
||||
"selectPeriodLabel": "Seleccionar Periodo"
|
||||
"selectPeriodLabel": "Seleccionar Periodo",
|
||||
"info": {
|
||||
"title": "Información",
|
||||
"memory": "Memoria",
|
||||
"uptime": "Tiempo de actividad",
|
||||
"activationTime": "Tiempo de creación de Cloudron",
|
||||
"platformVersion": "Versión de plataforma",
|
||||
"product": "Producto",
|
||||
"vendor": "Vendedor"
|
||||
},
|
||||
"graphs": {
|
||||
"title": "Gráficos"
|
||||
}
|
||||
},
|
||||
"support": {
|
||||
"remoteSupport": {
|
||||
@@ -1389,9 +1430,14 @@
|
||||
"sshCheckbox": "Permitir que los ingenieros de soporte se conecten a este servidor a través de SSH",
|
||||
"emailPlaceholder": "Si es necesario, proporciona una dirección de correo electrónico diferente de la anterior para contactarte",
|
||||
"emailVerifyAction": "Verificar ahora",
|
||||
"emailNotVerified": "El correo electrónico de su cuenta cloudron.io {{email}} no está verificado. Verifíquelo para abrir tickets de soporte."
|
||||
"emailNotVerified": "El correo electrónico de su cuenta cloudron.io {{email}} no está verificado. Verifíquelo para abrir tickets de soporte.",
|
||||
"typeBilling": "Problema de facturación"
|
||||
},
|
||||
"title": "Soporte"
|
||||
"title": "Soporte",
|
||||
"help": {
|
||||
"title": "Ayuda",
|
||||
"description": "Utiliza los siguientes recursos para obtener ayuda y soporte:\n* [Foro de Cloudron]({{ forumLink }}) - Utiliza las categorías específicas de Soporte y Aplicación si tiene preguntas.\n* [Base de conocimientos y documentos de Cloudron]({{ docsLink }})\n* [API y empaquetado de aplicaciones personalizadas]({{ packagingLink }})\n"
|
||||
}
|
||||
},
|
||||
"volumes": {
|
||||
"removeVolumeDialog": {
|
||||
@@ -1428,7 +1474,11 @@
|
||||
"title": "Actualizar Volumen {{ volume }}"
|
||||
},
|
||||
"tooltipEdit": "Editar Volumen",
|
||||
"remountActionTooltip": "Volver a montar Volumen"
|
||||
"remountActionTooltip": "Volver a montar Volumen",
|
||||
"editVolumeDialog": {
|
||||
"title": "Editar volumen {{ name }}"
|
||||
},
|
||||
"editActionTooltip": "Editar Volumen"
|
||||
},
|
||||
"eventlog": {
|
||||
"filterAllEvents": "Todos los Eventos",
|
||||
@@ -1462,7 +1512,8 @@
|
||||
"renameDialog": {
|
||||
"title": "Renombrar {{ fileName }}",
|
||||
"newName": "Nuevo Nombre",
|
||||
"rename": "Renombrar"
|
||||
"rename": "Renombrar",
|
||||
"reallyOverwrite": "Ya existe un archivo con ese nombre. ¿Sobrescribir el archivo existente?"
|
||||
},
|
||||
"chownDialog": {
|
||||
"newOwner": "Nuevo propietario",
|
||||
@@ -1507,7 +1558,8 @@
|
||||
"copy": "Copiar",
|
||||
"paste": "Pegar",
|
||||
"selectAll": "Seleccionar todo",
|
||||
"download": "Descargar"
|
||||
"download": "Descargar",
|
||||
"open": "Abrir"
|
||||
},
|
||||
"mtime": "Modificado"
|
||||
},
|
||||
@@ -1522,12 +1574,26 @@
|
||||
},
|
||||
"extract": {
|
||||
"error": "La extracción falló: {{ message }}"
|
||||
}
|
||||
},
|
||||
"extractionInProgress": "Extracción en progreso",
|
||||
"uploader": {
|
||||
"exitWarning": "Subida en progreso... ¿quieres realmente cerrar esta página?",
|
||||
"uploading": "Subiendo"
|
||||
},
|
||||
"textEditor": {
|
||||
"undo": "Deshacer",
|
||||
"redo": "Rehacer",
|
||||
"save": "Guardar"
|
||||
},
|
||||
"pasteInProgress": "Pegado en progreso",
|
||||
"deleteInProgress": "Borrado en progreso"
|
||||
},
|
||||
"logs": {
|
||||
"download": "Descarga los Registros Completos",
|
||||
"clear": "Borrar Vista",
|
||||
"title": "Registros"
|
||||
"title": "Registros",
|
||||
"notFoundError": "No existe esa tarea o aplicación",
|
||||
"logsGoneError": "Archivo(s) de registro no encontrados"
|
||||
},
|
||||
"email": {
|
||||
"signature": {
|
||||
@@ -1763,7 +1829,7 @@
|
||||
"newPassword": {
|
||||
"errorLength": "La contraseña debe tener al menos 8 y un máximo de 265 caracteres",
|
||||
"title": "Establecer nueva contraseña",
|
||||
"password": "Nueva contraseña",
|
||||
"password": "Nueva Contraseña",
|
||||
"passwordRepeat": "Repetir Contraseña",
|
||||
"errorMismatch": "Las contraseñas no coinciden"
|
||||
},
|
||||
@@ -1823,7 +1889,7 @@
|
||||
"username": "Nombre de usuario",
|
||||
"password": "Contraseña",
|
||||
"2faToken": "Token 2FA (si está habilitado)",
|
||||
"signInAction": "Iniciar sesión",
|
||||
"signInAction": "Iniciar Sesión",
|
||||
"resetPasswordAction": "Resetear contraseña",
|
||||
"errorIncorrect2FAToken": "El token 2FA es inválido",
|
||||
"errorInternal": "Error interno, prueba de nuevo más tarde"
|
||||
@@ -1879,5 +1945,6 @@
|
||||
"newClient": "Nuevo cliente",
|
||||
"empty": "No hay clientes aún"
|
||||
}
|
||||
}
|
||||
},
|
||||
"automation": "Automatización"
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"auth": {
|
||||
"nosso": "Se connecter avec un compte dédié",
|
||||
"email": "Se connecter avec une adresse email",
|
||||
"sso": "Se connecter avec vos identifiants Cloudron"
|
||||
"sso": "Se connecter avec vos identifiants Cloudron",
|
||||
"openid": "Se connecter avec Cloudron OpenID"
|
||||
},
|
||||
"addAppAction": "Ajouter Application",
|
||||
"addAppproxyAction": "Ajouter Proxy d'application",
|
||||
@@ -39,7 +40,8 @@
|
||||
"cancel": "Annuler",
|
||||
"save": "Sauvegarder",
|
||||
"no": "Non",
|
||||
"yes": "Oui"
|
||||
"yes": "Oui",
|
||||
"delete": "Supprimer"
|
||||
},
|
||||
"username": "Nom d'utilisateur",
|
||||
"actions": "Actions",
|
||||
@@ -55,7 +57,8 @@
|
||||
},
|
||||
"action": {
|
||||
"logs": "Journaux",
|
||||
"reboot": "Redémarrer"
|
||||
"reboot": "Redémarrer",
|
||||
"showLogs": "Afficher Journaux"
|
||||
},
|
||||
"rebootDialog": {
|
||||
"rebootAction": "Redémarrer maintenant",
|
||||
@@ -87,7 +90,9 @@
|
||||
},
|
||||
"disableAction": "Désactiver",
|
||||
"enableAction": "Activer",
|
||||
"loadingPlaceholder": "Chargement"
|
||||
"loadingPlaceholder": "Chargement",
|
||||
"settings": "Paramètres",
|
||||
"saveAction": "Sauvegarde"
|
||||
},
|
||||
"users": {
|
||||
"title": "Annuaire des utilisateurs",
|
||||
@@ -108,8 +113,7 @@
|
||||
"setGhostTooltip": "Emprunter l'identité",
|
||||
"invitationTooltip": "Envoyer une invitation à l'utilisateur",
|
||||
"mailmanagerTooltip": "Cet utilisateur peut gérer les utilisateurs et les boîtes mail",
|
||||
"count": "Total des utilisateurs : {{ count }}",
|
||||
"makeLocalTooltip": "Rendre l'utilisateur local"
|
||||
"count": "Total des utilisateurs : {{ count }}"
|
||||
},
|
||||
"newUserAction": "Nouvel utilisateur",
|
||||
"groups": {
|
||||
@@ -120,7 +124,7 @@
|
||||
"externalLdapTooltip": "Depuis un annuaire LDAP externe"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Paramètres",
|
||||
"title": "Paramètres Utilisateur",
|
||||
"allowProfileEditCheckbox": "Autoriser les utilisateurs à modifier leur nom et leur adresse email",
|
||||
"saveAction": "Enregistrer",
|
||||
"subscriptionRequired": "Ces fonctionnalités sont uniquement disponibles dans la version payante.",
|
||||
@@ -140,7 +144,6 @@
|
||||
"groupnameField": "Champ nom du groupe",
|
||||
"syncGroups": "Groupes synchronisés",
|
||||
"filter": "Filtre",
|
||||
"subscriptionRequired": "Cette fonctionnalité est disponible uniquement dans la version payante.",
|
||||
"acceptSelfSignedCert": "Accepter le certificat auto-signé",
|
||||
"usernameField": "Champ nom d'utilisateur",
|
||||
"groupFilter": "Filtre des groupes",
|
||||
@@ -153,7 +156,8 @@
|
||||
"description": "Cloudron va importer les utilisateurs et les groupes depuis un annuaire LDAP externe ou Active Directory. La vérification du mot de passe pour l'authentification de ces utilisateurs se fait via le serveur externe. La synchronisation ne s'exécute pas automatiquement, elle doit être lancée manuellement.",
|
||||
"subscriptionRequiredAction": "Paramétrer mon abonnement maintenant",
|
||||
"providerOther": "Autre",
|
||||
"providerDisabled": "Désactivé"
|
||||
"providerDisabled": "Désactivé",
|
||||
"disableWarning": "La source d'authentification de tous les utilisateurs existants sera réinitialisée pour utiliser la base de données locale."
|
||||
},
|
||||
"role": {
|
||||
"usermanager": "Gestionnaire",
|
||||
@@ -185,7 +189,9 @@
|
||||
"errorInvalidEmail": "Cette adresse email est invalide",
|
||||
"usernamePlaceholder": "Optionnel. Si laissé vide, l'utilisateur peut en choisir un lors de la première connexion",
|
||||
"fallbackEmailPlaceholder": "Optionnel. Si laissé vide, ce sera l'adresse email principale qui sera utilisée",
|
||||
"displayNamePlaceholder": "Optionnel. Si laissé vide, l'utilisateur peut en choisir un lors de la création du compte"
|
||||
"displayNamePlaceholder": "Optionnel. Si laissé vide, l'utilisateur peut en choisir un lors de la création du compte",
|
||||
"external2FA": "La configuration multi-facteur est gérée par une source externe",
|
||||
"ldapGroups": "Groupes LDAP"
|
||||
},
|
||||
"group": {
|
||||
"errorNameRequired": "Un nom est nécessaire",
|
||||
@@ -266,15 +272,9 @@
|
||||
"title": "Lien d'invitation envoyé",
|
||||
"body": "Email envoyé à {{ email }}"
|
||||
},
|
||||
"makeLocalDialog": {
|
||||
"description": "Cela migrera l'utilisateur du répertoire externe vers le Cloudron.",
|
||||
"submitAction": "Rendre local",
|
||||
"title": "Rendre cet utilisateur local",
|
||||
"warning": "Une réinitialisation du mot de passe sera initiée pour définir un mot de passe local pour cet utilisateur."
|
||||
},
|
||||
"exposedLdap": {
|
||||
"secret": {
|
||||
"label": "Mot de passe de liaison",
|
||||
"label": "Mot de passe Bind",
|
||||
"description": "Toutes les requêtes LDAP doivent être authentifiées avec ce secret et le DN utilisateur <i>{{ userDN }}</i>",
|
||||
"url": "URL du serveur"
|
||||
},
|
||||
@@ -285,7 +285,8 @@
|
||||
"placeholder": "Adresse IP séparée par ligne ou sous-réseau",
|
||||
"label": "Accès restreint"
|
||||
},
|
||||
"title": "Serveur d'annuaire"
|
||||
"title": "Serveur d'annuaire",
|
||||
"cloudflarePortWarning": "Le proxy Cloudflare doit être désactivé sur le domaine du tableau de bord pour accéder au service LDAP"
|
||||
},
|
||||
"userImportDialog": {
|
||||
"title": "Importer des utilisateurs",
|
||||
@@ -348,7 +349,10 @@
|
||||
"changeEmail": {
|
||||
"errorEmailInvalid": "Cette adresse email est invalide",
|
||||
"title": "Modifier l'adresse email principale",
|
||||
"errorEmailRequired": "Une adresse email valide est nécessaire"
|
||||
"errorEmailRequired": "Une adresse email valide est nécessaire",
|
||||
"email": "Nouvelle adresse e-mail",
|
||||
"password": "Mot de passe pour confirmation",
|
||||
"errorWrongPassword": "Mauvais mot de passe"
|
||||
},
|
||||
"createAppPassword": {
|
||||
"copyNow": "Veillez à copier le mot de passe maintenant. Il ne s'affichera plus pour des raisons de sécurité.",
|
||||
@@ -419,7 +423,8 @@
|
||||
},
|
||||
"changeBackgroundImage": {
|
||||
"title": "Définir l'image d'arrière-plan"
|
||||
}
|
||||
},
|
||||
"enable2FANotAvailable": "Non disponible pour les utilisateurs provenant d'une source d'authentification externe"
|
||||
},
|
||||
"backups": {
|
||||
"title": "Sauvegardes",
|
||||
@@ -502,7 +507,8 @@
|
||||
"port": "Port",
|
||||
"cifsSealSupport": "Utilisez le cryptage du sceau. Nécessite au moins SMB v3",
|
||||
"chown": "Le système de fichiers distant prend en charge chown",
|
||||
"encryptedFilenames": "Crypter les noms de fichiers"
|
||||
"encryptedFilenames": "Crypter les noms de fichiers",
|
||||
"encryptFilenames": "Chiffré les nom de fichiers"
|
||||
},
|
||||
"backupDetails": {
|
||||
"title": "Informations sur la sauvegarde",
|
||||
@@ -544,7 +550,8 @@
|
||||
"preserved": {
|
||||
"description": "Sauvegarde persistante quelle que soit la politique de rétention",
|
||||
"tooltip": "Cela préservera également le courrier et les sauvegardes d'application {{ appsLength }}."
|
||||
}
|
||||
},
|
||||
"remotePath": "Chemin d'accès à distance"
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
@@ -554,7 +561,7 @@
|
||||
"location": "Emplacement",
|
||||
"title": "Changer l'emplacement du serveur de messagerie",
|
||||
"locationPlaceholder": "Laisser vide pour utiliser le nom de domaine nu",
|
||||
"description": "Cloudron effectuera les modifications DNS nécessaires pour l'ensemble des domaines et redémarrera le serveur de messagerie. Les clients de messagerie sur ordinateur et sur mobile doivent être reconfigurés pour que ce nouvel emplacement soit utilisé comme serveur IMAP et SMTP."
|
||||
"description": "Cela déplacera le serveur IMAP et SMTP vers l'emplacement choisi."
|
||||
},
|
||||
"eventlog": {
|
||||
"details": "Détails",
|
||||
@@ -600,7 +607,8 @@
|
||||
"solrEnabled": "Activé",
|
||||
"solrRunning": "Actif",
|
||||
"acl": "Adresse ACL (liste de contrôle d'accès)",
|
||||
"aclOverview": "{{ dnsblZonesCount }} liste(s) DNSBL"
|
||||
"aclOverview": "{{ dnsblZonesCount }} liste(s) DNSBL",
|
||||
"virtualAllMail": "Dossier \"Tout les Emails\""
|
||||
},
|
||||
"domains": {
|
||||
"disabled": "Désactivé",
|
||||
@@ -663,6 +671,10 @@
|
||||
},
|
||||
"action": {
|
||||
"queue": "File d'attente"
|
||||
},
|
||||
"changeVirtualAllMailDialog": {
|
||||
"title": "Dossier \"Tout les Emails\"",
|
||||
"description": "Le dossier \"Tout les E-mails\" est un dossier contenant tout les e-mails de votre boite de réception. Ce dossier peut être utile pour les clients e-mails ne supportant pas les dossiers imbriqués."
|
||||
}
|
||||
},
|
||||
"network": {
|
||||
@@ -679,7 +691,8 @@
|
||||
},
|
||||
"dyndns": {
|
||||
"title": "DNS dynamique",
|
||||
"description": "Activez cette option pour conserver tous vos enregistrements DNS synchronisés avec une adresse IP dynamique. Cette option est utile lorsque Cloudron fonctionne avec un réseau dont l'adresse IP publique change fréquemment, comme dans le cas d'une connexion domestique."
|
||||
"description": "Activez cette option pour conserver tous vos enregistrements DNS synchronisés avec une adresse IP dynamique. Cette option est utile lorsque Cloudron fonctionne avec un réseau dont l'adresse IP publique change fréquemment, comme dans le cas d'une connexion domestique.",
|
||||
"showLogsAction": "Afficher les journaux"
|
||||
},
|
||||
"ip": {
|
||||
"configure": "Paramétrer",
|
||||
@@ -705,7 +718,13 @@
|
||||
"address": "Adresse IPv6",
|
||||
"title": "IPv6",
|
||||
"description": "Cloudron utilise cette adresse IPv6 pour configurer les enregistrements DNS AAAA.\n"
|
||||
}
|
||||
},
|
||||
"trustedIps": {
|
||||
"description": "Les en-têtes HTTP provenant d'adresses IP correspondantes seront considérés comme sûrs",
|
||||
"title": "Configurer les adresses IP de Confiance",
|
||||
"summary": "{{ trustCount }} adresses IP de confiance"
|
||||
},
|
||||
"trustedIpRanges": "Adresses et plages d'IP de confiance. "
|
||||
},
|
||||
"settings": {
|
||||
"title": "Paramètres",
|
||||
@@ -809,7 +828,12 @@
|
||||
"subscriptionRequiredDescription": "Vous devriez trouver votre réponse dans notre <a href=\"{{ supportViewLink }}\" target=\"_blank\">documentation</a>, vous pouvez également poser votre question sur le <a href=\"{{ forumLink }}\" target=\"_blank\">forum</a>.",
|
||||
"title": "Ticket",
|
||||
"emailVerifyAction": "Confirmer maintenant",
|
||||
"emailNotVerified": "L'adresse email de votre compte Cloudron.io {{ email }} n'a pas encore été confirmée. Veuillez la valider pour ouvrir des tickets d'incident."
|
||||
"emailNotVerified": "L'adresse email de votre compte Cloudron.io {{ email }} n'a pas encore été confirmée. Veuillez la valider pour ouvrir des tickets d'incident.",
|
||||
"typeBilling": "Problème de facturation"
|
||||
},
|
||||
"help": {
|
||||
"description": "Veuillez utiliser les ressources suivantes pour obtenir de l'aide\n* [Forum Cloudron]({{ forumLink }}) - Veuillez utiliser les catégories d'assistance et d'applications spécifiques pour vos questions.\n* [Documentation et base de connaissances de Cloudron]({{ docsLink }})\n* [Packaging d'applications personnalisées et API]({{ packagingLink }})\n",
|
||||
"title": "Aide"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
@@ -859,7 +883,10 @@
|
||||
"titleLogin": "Se connecter à Cloudron.io",
|
||||
"description": "Ce compte permet d'accéder à l'App Store et de gérer votre abonnement",
|
||||
"2faToken": "Jeton 2FA (si activé)",
|
||||
"intendedUse": "Type d'usage"
|
||||
"intendedUse": "Type d'usage",
|
||||
"setupWithTokenAction": "Configuration",
|
||||
"setupToken": "Configuration Jeton",
|
||||
"titleToken": "Se connecter avec un Jeton"
|
||||
},
|
||||
"title": "App Store",
|
||||
"appNotFoundDialog": {
|
||||
@@ -914,7 +941,8 @@
|
||||
"packageVersion": "Version du package",
|
||||
"appId": "ID de l'application",
|
||||
"description": "Nom et version de l'application",
|
||||
"title": "Informations sur l'application"
|
||||
"title": "Informations sur l'application",
|
||||
"repository": "Dépot de paquets"
|
||||
},
|
||||
"auto": {
|
||||
"title": "Mises à jour automatiques",
|
||||
@@ -923,7 +951,8 @@
|
||||
"disabled": "Les mises à jour automatiques sont actuellement désactivées.",
|
||||
"enabled": "Les mises à jour automatiques sont actuellement activées.",
|
||||
"description": "Cloudron vérifie régulièrement les mises à jour disponibles dans l'App Store. Si vous désactivez les mises à jour automatiques, veillez à les faire manuellement."
|
||||
}
|
||||
},
|
||||
"noUpdates": "Aucune nouvelle mise à jour disponible"
|
||||
},
|
||||
"backupsTabTitle": "Sauvegardes",
|
||||
"storage": {
|
||||
@@ -933,13 +962,20 @@
|
||||
"noMounts": "Aucun volume n'est monté.",
|
||||
"volume": "Volume",
|
||||
"readOnly": "En lecture seule",
|
||||
"title": "Montages"
|
||||
"title": "Montages",
|
||||
"permissions": {
|
||||
"label": "Permissions",
|
||||
"readOnly": "Lecture seule",
|
||||
"readWrite": "Lecture et écriture"
|
||||
}
|
||||
},
|
||||
"appdata": {
|
||||
"moveAction": "Déplacer les données",
|
||||
"dataDirPlaceholder": "Laisser vide pour utiliser la plateforme par défaut",
|
||||
"description": "Si le serveur manque d'espace disque, utilisez-le pour déplacer les données de l'application vers un <a href=\"/#/volumes\">volume</a>. Toutes les données ici font partie de la sauvegarde de l'application.",
|
||||
"title": "Données de l'application"
|
||||
"title": "Données de l'application",
|
||||
"diskUsage": "L'application utilise actuellement {{ size }} de stockage (en date du {{ date }}).",
|
||||
"mountTypeWarning": "Le système de fichiers de destination doit prendre en charge les autorisations et la propriété des fichiers pour que le transfert fonctionne"
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
@@ -952,7 +988,8 @@
|
||||
"disableIndexingAction": "Désactiver l'indexation",
|
||||
"txtPlaceholder": "Laisser vide pour autoriser les robots à indexer cette application",
|
||||
"title": "Robots.txt"
|
||||
}
|
||||
},
|
||||
"hstsPreload": "Activer HSTS pour ce site et tous les sous-domaines"
|
||||
},
|
||||
"updateDialog": {
|
||||
"updateAction": "Mettre à jour",
|
||||
@@ -1046,7 +1083,8 @@
|
||||
"uploadAction": "Charger le fichier de configuration de la sauvegarde",
|
||||
"description": "Toutes les données créées depuis la dernière sauvegarde connue seront définitivement perdues. Il est fortement recommandé de sauvegarder les données actuelles avant de lancer un import.",
|
||||
"title": "Importer la sauvegarde",
|
||||
"importAction": "Importer"
|
||||
"importAction": "Importer",
|
||||
"remotePath": "Chemin de la sauvegarde"
|
||||
},
|
||||
"repairDialog": {
|
||||
"fromBackup": "Restaurer depuis la sauvegarde :",
|
||||
@@ -1117,7 +1155,8 @@
|
||||
"time": "Créée le",
|
||||
"packageVersion": "Version du package",
|
||||
"description": "Les sauvegardes sont des instantanés complets de l'application. Vous pouvez utiliser les sauvegardes pour restaurer ou cloner l'application.",
|
||||
"title": "Sauvegardes"
|
||||
"title": "Sauvegardes",
|
||||
"downloadBackupTooltip": "Télécharger la sauvegarde"
|
||||
}
|
||||
},
|
||||
"graphs": {
|
||||
@@ -1129,7 +1168,9 @@
|
||||
"12h": "12 heures",
|
||||
"6h": "6 heures"
|
||||
},
|
||||
"diskTitle": "Utilisation du disque"
|
||||
"diskTitle": "Utilisation du disque",
|
||||
"diskIOTotal": "total: lecture {{ read }} / écriture {{ write }}",
|
||||
"networkIOTotal": "total : entrant {{ inbound }} / sortant {{ outbound }}"
|
||||
},
|
||||
"resources": {
|
||||
"memory": {
|
||||
@@ -1218,12 +1259,25 @@
|
||||
"label": "Étiquette",
|
||||
"clearIconAction": "Effacer Icône",
|
||||
"clearIconDescription": "Cela récupérera le favicon de l'application."
|
||||
},
|
||||
"servicesTabTitle": "Services",
|
||||
"turn": {
|
||||
"enable": "Configurer l'application pour utiliser le serveur TURN intégré",
|
||||
"disable": "Ne pas configurer les paramètres TURN de l'application. Les paramètres TURN de l'application sont laissés à leur valeurs par défaut. Vous pouvez les configurer à l'intérieur de l'application.",
|
||||
"title": "Configuration de TURN"
|
||||
},
|
||||
"redis": {
|
||||
"title": "Configuration de Redis",
|
||||
"enable": "Configurer l'application pour utiliser Redis",
|
||||
"disable": "Désactiver Redis"
|
||||
}
|
||||
},
|
||||
"logs": {
|
||||
"title": "Journaux",
|
||||
"download": "Télécharger l'ensemble des journaux",
|
||||
"clear": "Nettoyer"
|
||||
"clear": "Nettoyer",
|
||||
"notFoundError": "Aucune tâche ou application de ce type",
|
||||
"logsGoneError": "Fichier(s) journal(s) introuvable(s)"
|
||||
},
|
||||
"volumes": {
|
||||
"name": "Nom",
|
||||
@@ -1260,7 +1314,11 @@
|
||||
"title": "Mettre à jour le volume {{ volume }}"
|
||||
},
|
||||
"mountStatus": "Statut du montage",
|
||||
"type": "Type"
|
||||
"type": "Type",
|
||||
"editVolumeDialog": {
|
||||
"title": "Modifier le volume {{ name }}"
|
||||
},
|
||||
"editActionTooltip": "Modifier le volume"
|
||||
},
|
||||
"lang": {
|
||||
"en": "Anglais",
|
||||
@@ -1274,7 +1332,8 @@
|
||||
"zh_Hans": "Chinois (Simplifié)",
|
||||
"es": "Espagnol",
|
||||
"ru": "Russe",
|
||||
"pt": "Portugais"
|
||||
"pt": "Portugais",
|
||||
"da": "Danois"
|
||||
},
|
||||
"email": {
|
||||
"mailboxboxDialog": {
|
||||
@@ -1519,10 +1578,19 @@
|
||||
"vultrToken": "Token Vultr",
|
||||
"wellKnownDescription": "Les valeurs seront utilisées par Cloudron pour répondre aux URL <code>/.well-known/</code>. Notez qu'une application doit être disponible sur le domaine nu <code>{{ domaine }}</code> pour que cela fonctionne. Consultez la <a href=\"{{docsLink}}\" target=\"_blank\">documentation</a> pour plus d'informations.",
|
||||
"hetznerToken": "Token Hetzner",
|
||||
"jitsiHostname": "Emplacement de Jitsi"
|
||||
"jitsiHostname": "Emplacement de Jitsi",
|
||||
"cloudflareDefaultProxyStatus": "Activer le proxy pour les nouveaux enregistrements DNS",
|
||||
"porkbunApikey": "Clé API",
|
||||
"porkbunSecretapikey": "Clé API secrète",
|
||||
"dnsimpleAccessToken": "Jeton d'accès",
|
||||
"ovhEndpoint": "Point de terminaison",
|
||||
"bunnyAccessKey": "Bunny Access Key",
|
||||
"ovhConsumerKey": "Consumer Key",
|
||||
"ovhAppKey": "Application Key",
|
||||
"ovhAppSecret": "Application Secret"
|
||||
},
|
||||
"changeDashboardDomain": {
|
||||
"description": "Cette action entraînera le déplacement du tableau de bord et du serveur de messagerie vers le sous-domaine <code>my</code> du domaine sélectionné.",
|
||||
"description": "Cette action entraînera le déplacement du tableau de bord vers le sous-domaine <code>my</code> du domaine sélectionné.",
|
||||
"showLogsAction": "Afficher les journaux",
|
||||
"cancelAction": "Annuler",
|
||||
"changeAction": "Changer le domaine",
|
||||
@@ -1548,7 +1616,8 @@
|
||||
"domainWellKnown": {
|
||||
"title": "Emplacements Well-Known de {{ domain }}"
|
||||
},
|
||||
"tooltipWellKnown": "Définir des emplacements Well-Known"
|
||||
"tooltipWellKnown": "Définir des emplacements Well-Known",
|
||||
"count": "Nombre de domaines: {{ count }}"
|
||||
},
|
||||
"branding": {
|
||||
"footer": {
|
||||
@@ -1629,7 +1698,8 @@
|
||||
"download": "Télécharger",
|
||||
"extract": "Extraire ici",
|
||||
"chown": "Modifier la propriété",
|
||||
"rename": "Renommer"
|
||||
"rename": "Renommer",
|
||||
"open": "Ouvrir"
|
||||
},
|
||||
"symlink": "Symlink vers {{ target }}",
|
||||
"empty": "Aucun fichier",
|
||||
@@ -1675,7 +1745,8 @@
|
||||
"renameDialog": {
|
||||
"rename": "Renommer",
|
||||
"newName": "Nouveau nom",
|
||||
"title": "Renommer {{ fileName }}"
|
||||
"title": "Renommer {{ fileName }}",
|
||||
"reallyOverwrite": "Un fichier portant ce nom existe déjà. Écraser le fichier existant ?"
|
||||
},
|
||||
"newFileDialog": {
|
||||
"create": "Créer",
|
||||
@@ -1688,7 +1759,19 @@
|
||||
"removeDialog": {
|
||||
"reallyDelete": "Voulez-vous vraiment supprimer ces fichiers ?"
|
||||
},
|
||||
"title": "Gestionnaire de fichiers"
|
||||
"title": "Gestionnaire de fichiers",
|
||||
"uploader": {
|
||||
"uploading": "Téléversement",
|
||||
"exitWarning": "Téléversement toujours en cours. Voulez-vous vraiment fermer cette page ?"
|
||||
},
|
||||
"deleteInProgress": "Suppression en cours",
|
||||
"textEditor": {
|
||||
"undo": "Annuler",
|
||||
"redo": "Refaire",
|
||||
"save": "Enregistrer"
|
||||
},
|
||||
"extractionInProgress": "Décompression en cours",
|
||||
"pasteInProgress": "Collage en cours"
|
||||
},
|
||||
"terminal": {
|
||||
"contextmenu": {
|
||||
@@ -1729,7 +1812,8 @@
|
||||
"selectPeriodLabel": "Période sélectionnée",
|
||||
"cpuUsage": {
|
||||
"graphTitle": "Pourcentage",
|
||||
"title": "Utilisation du microprocesseur"
|
||||
"title": "Utilisation du microprocesseur",
|
||||
"graphSubtext": "Seules les applications utilisant plus de {{ threshold }} de processeur sont affichées"
|
||||
},
|
||||
"systemMemory": {
|
||||
"graphSubtext": "Seules les applications utilisant plus de 1GB de mémoire sont affichées",
|
||||
@@ -1743,9 +1827,22 @@
|
||||
"title": "Utilisation du disque",
|
||||
"usedInfo": "{{ used }} utilisé de {{ size }}",
|
||||
"uninstalledApp": "Désinstaller App",
|
||||
"diskSpeed": "Vitesse : {{ speed }} MB/sec"
|
||||
"diskSpeed": "Vitesse : {{ speed }} MB/sec",
|
||||
"volumeContent": "Ce disque est le volume <code>{{ name }}</code>"
|
||||
},
|
||||
"title": "Info système"
|
||||
"title": "Info système",
|
||||
"info": {
|
||||
"platformVersion": "Version de la Plate-forme",
|
||||
"vendor": "Vendeur",
|
||||
"product": "Produit",
|
||||
"memory": "Mémoire",
|
||||
"uptime": "Durée de fonctionnement",
|
||||
"activationTime": "Heure de création de Cloudron",
|
||||
"title": "Informations"
|
||||
},
|
||||
"graphs": {
|
||||
"title": "Graphiques"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"refresh": "Rafraîchir",
|
||||
@@ -1800,7 +1897,9 @@
|
||||
"password": "Mot de passe",
|
||||
"username": "Nom d'utilisateur",
|
||||
"errorIncorrectCredentials": "Nom d'utilisateur ou mot de passe incorrect",
|
||||
"loginTo": "Se connecter à"
|
||||
"loginTo": "Se connecter à",
|
||||
"errorIncorrect2FAToken": "Le jeton 2FA n'est pas valide",
|
||||
"errorInternal": "Erreur interne, réessayer ultérieurement"
|
||||
},
|
||||
"newLoginEmail": {
|
||||
"salutation": "Bonjour <%= user %>,",
|
||||
@@ -1816,5 +1915,43 @@
|
||||
"mounts": {
|
||||
"description": "Les applications peuvent accéder aux <a href=\"/#/volumes\">volumes</a> montés via le répertoire <code>/media/{volume name}</code>. Ces données ne sont pas incluses dans la sauvegarde de l'application."
|
||||
}
|
||||
}
|
||||
},
|
||||
"oidc": {
|
||||
"client": {
|
||||
"signingAlgorithm": "Algorithme de signature",
|
||||
"name": "Nom",
|
||||
"id": "ID du client",
|
||||
"secret": "Secret du client",
|
||||
"loginRedirectUri": "Url de retour post connexion (séparées par des virgules s'il y en a plus d'une)",
|
||||
"logoutRedirectUri": "Url de retour après déconnexion (facultatif)"
|
||||
},
|
||||
"description": "Cloudron peut agir en tant que fournisseur OpenID Connect pour les applications internes et les services externes.",
|
||||
"deleteClientDialog": {
|
||||
"description": "Cela déconnectera toutes les applications OpenID externes de ce Cloudron utilisant cet identifiant client.",
|
||||
"title": "Supprimer définitivement le client {{ client }} ?"
|
||||
},
|
||||
"newClientDialog": {
|
||||
"title": "Ajouter un client",
|
||||
"description": "Ajouter de nouveaux paramètres pour le client OpenID connect.",
|
||||
"createAction": "Créer"
|
||||
},
|
||||
"title": "OpenID Connect Provider",
|
||||
"editClientDialog": {
|
||||
"title": "Modifier le client {{ client }}"
|
||||
},
|
||||
"env": {
|
||||
"discoveryUrl": "URL de découverte",
|
||||
"logoutUrl": "URL de déconnexion",
|
||||
"profileEndpoint": "Point de terminaison pour le profil",
|
||||
"keysEndpoint": "Point de terminaison pour les clés",
|
||||
"tokenEndpoint": "Point de terminaison pour les jetons",
|
||||
"authEndpoint": "Point de terminaison pour l'authentification"
|
||||
},
|
||||
"clients": {
|
||||
"title": "Clients",
|
||||
"newClient": "Nouveau client",
|
||||
"empty": "Aucun client pour le moment"
|
||||
}
|
||||
},
|
||||
"automation": "Automatisation"
|
||||
}
|
||||
|
||||
@@ -932,7 +932,6 @@
|
||||
"server": "URL del Server",
|
||||
"noopInfo": "L'autenticazione LDAP non è configurata.",
|
||||
"subscriptionRequiredAction": "Attiva un piano a pagamento",
|
||||
"subscriptionRequired": "Questa funzionalità è disponibile solo nei piani a pagamento.",
|
||||
"description": "Cloudron sincronizzerà utenti e gruppi da un server LDAP o ActiveDirectory esterni. La verifica della password per l'autenticazione di tali utenti viene eseguita sul server esterno. La sincronizzazione non viene eseguita automaticamente ma deve essere attivata manualmente.",
|
||||
"auth": "Auth",
|
||||
"groupnameField": "Campo Groupname",
|
||||
|
||||
@@ -22,13 +22,17 @@
|
||||
"auth": {
|
||||
"nosso": "Log in met specifiek account",
|
||||
"sso": "Log in met Cloudron aanmeldgegevens",
|
||||
"email": "Log in met e-mailadres"
|
||||
"email": "Log in met e-mailadres",
|
||||
"openid": "Log in met Cloudron OpenID"
|
||||
},
|
||||
"addAppAction": "App toevoegen",
|
||||
"addAppproxyAction": "App Proxy toevoegen",
|
||||
"addApplinkAction": "App link toevoegen",
|
||||
"filter": {
|
||||
"clearAll": "Alles verwijderen"
|
||||
},
|
||||
"apps": {
|
||||
"count": "Totaal apps: {{ count }}"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
@@ -80,7 +84,8 @@
|
||||
"justNow": "zojuist",
|
||||
"yeserday": "Gisteren",
|
||||
"minutesAgo": "{{ m }} minuten geleden",
|
||||
"hoursAgo": "{{ h }} uur geleden"
|
||||
"hoursAgo": "{{ h }} uur geleden",
|
||||
"never": "Nooit"
|
||||
},
|
||||
"navbar": {
|
||||
"users": "Gebruikers"
|
||||
@@ -164,7 +169,10 @@
|
||||
"loginAction": "Inloggen",
|
||||
"createAccountAction": "Account aanmaken",
|
||||
"switchToSignUpAction": "Nog geen account? Registreer",
|
||||
"switchToLoginAction": "Al een account? Log in"
|
||||
"switchToLoginAction": "Al een account? Log in",
|
||||
"setupWithTokenAction": "Instellen",
|
||||
"setupToken": "Instel Token",
|
||||
"titleToken": "Inloggen met Instel Token"
|
||||
},
|
||||
"searchPlaceholder": "Zoek voor alternatieven zoals Github, Dropbox, Slack, Trello, …",
|
||||
"appNotFoundDialog": {
|
||||
@@ -197,8 +205,7 @@
|
||||
"invitationTooltip": "Gebruiker uitnodigen",
|
||||
"setGhostTooltip": "Nabootsen",
|
||||
"mailmanagerTooltip": "Deze gebruiker kan gebruikers en mailboxen beheren",
|
||||
"count": "Totaal gebruikers: {{ count }}",
|
||||
"makeLocalTooltip": "Maak gebruiker lokaal"
|
||||
"count": "Totaal gebruikers: {{ count }}"
|
||||
},
|
||||
"groups": {
|
||||
"title": "Groepen",
|
||||
@@ -218,7 +225,6 @@
|
||||
},
|
||||
"externalLdap": {
|
||||
"title": "Verbind met een externe lijst",
|
||||
"subscriptionRequired": "Deze functie is alleen beschikbaar voor betaalde abonnementen.",
|
||||
"subscriptionRequiredAction": "Neem nu een abonnement",
|
||||
"noopInfo": "LDAP authenticatie is niet geconfigureerd.",
|
||||
"provider": "Aanbieder",
|
||||
@@ -232,16 +238,17 @@
|
||||
"groupnameField": "Veld voor groepsnaam",
|
||||
"server": "Server URL",
|
||||
"showLogsAction": "Toon logbestanden",
|
||||
"syncAction": "Synchroniseer",
|
||||
"syncAction": "Sync",
|
||||
"configureAction": "Configureer",
|
||||
"bindUsername": "Bind DN/Username (optioneel)",
|
||||
"bindPassword": "Bind Password (optioneel)",
|
||||
"errorSelfSignedCert": "Server gebruikt een ongeldig of zelf-ondertekend certificaat.",
|
||||
"description": "Cloudron synchroniseert gebruikers en groepen van een extern LDAP of ActiveDirectory server. Wachtwoordverificatie vindt plaats door de externe server. De synchronisatie is niet automatisch en dient handmatig gestart te worden.",
|
||||
"description": "Deze instelling synchroniseert en authentificeert gebruikers en groepen van een extern LDAP of ActiveDirectory server. De synchronisatie is periodiek maar kan ook handmatig gestart worden.",
|
||||
"auth": "Authenticatie",
|
||||
"autocreateUsersOnLogin": "Maak automatisch gebruikers aan na inloggen bij deze Cloudron",
|
||||
"autocreateUsersOnLogin": "Maak automatisch gebruikers bij inloggen",
|
||||
"providerOther": "Anders",
|
||||
"providerDisabled": "Uitgeschakeld"
|
||||
"providerDisabled": "Uitgeschakeld",
|
||||
"disableWarning": "De authentificatie-bron van alle bestaande gebruikers zal worden omgezet naar authentificatie via de lokale wachtwoord database."
|
||||
},
|
||||
"subscriptionDialog": {
|
||||
"title": "Abonnement benodigd",
|
||||
@@ -270,7 +277,9 @@
|
||||
"errorInvalidUsername": "Dit is geen geldige gebruikersnaam",
|
||||
"usernamePlaceholder": "Optioneel. Indien niet ingevuld mag de gebruiker bij registratie zelf kiezen",
|
||||
"fallbackEmailPlaceholder": "Optioneel. Indien niet ingevoerd zal de primaire e-mail gebruikt worden",
|
||||
"displayNamePlaceholder": "Optioneel. Indien niet ingevoerd kan de gebruiker het kiezen tijdens eerste aanmelding"
|
||||
"displayNamePlaceholder": "Optioneel. Indien niet ingevoerd kan de gebruiker het kiezen tijdens eerste aanmelding",
|
||||
"external2FA": "2FA instellingen worden beheerd door een externe authenticatie bron",
|
||||
"ldapGroups": "LDAP Groepen"
|
||||
},
|
||||
"deleteUserDialog": {
|
||||
"deleteAction": "Verwijder",
|
||||
@@ -357,17 +366,18 @@
|
||||
"exposedLdap": {
|
||||
"ipRestriction": {
|
||||
"placeholder": "Regelgescheiden IP adres of Subnet",
|
||||
"description": "De lijstserver kan beperkt worden tot specifieke IP's of bereiken.",
|
||||
"description": "Beperk de toegang tot de Directory Server tot specifieke IP's of bereiken. Regels die starten met <code>#</code> worden beschouwd als commentaar.",
|
||||
"label": "Beperk toegang"
|
||||
},
|
||||
"enabled": "Ingeschakeld",
|
||||
"title": "Lijst server",
|
||||
"description": "Cloudron kan ingezet worden als gebruikerslijstserver voor externe applicaties.",
|
||||
"title": "Directory Server",
|
||||
"description": "Cloudron kan ingezet worden als gebruikers Directory Server voor externe applicaties.",
|
||||
"secret": {
|
||||
"label": "Koppel wachtwoord",
|
||||
"description": "Alle LDAP verzoeken moeten geauthentiseerd worden met dit geheim en de gebruiker DN <i>{{ userDN }}</i>",
|
||||
"url": "Server URL"
|
||||
}
|
||||
},
|
||||
"cloudflarePortWarning": "Cloudflare proxy moet uitgeschakeld zijn op het domein van het dashboard om de LDAP server te kunnen bereiken"
|
||||
},
|
||||
"userImportDialog": {
|
||||
"title": "Importeer gebruikers",
|
||||
@@ -391,12 +401,6 @@
|
||||
"all": "Alle gebruikers",
|
||||
"active": "Actieve gebruikers",
|
||||
"inactive": "Inactieve gebruikers"
|
||||
},
|
||||
"makeLocalDialog": {
|
||||
"title": "Maak deze gebruiker lokaal",
|
||||
"description": "De gebruiker wordt hiermee gemigreerd van de externe gebruikerslijst naar die van Cloudron.",
|
||||
"warning": "Een wachtwoord herstel wordt geïnitieerd om een lokaal wachtwoord in te stellen voor deze gebruiker.",
|
||||
"submitAction": "Maak lokaal"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
@@ -463,7 +467,10 @@
|
||||
"changeEmail": {
|
||||
"title": "Primair e-mailadres aanpassen",
|
||||
"errorEmailInvalid": "Het e-mailadres is niet geldig",
|
||||
"errorEmailRequired": "Een geldig e-mailadres is verplicht"
|
||||
"errorEmailRequired": "Een geldig e-mailadres is verplicht",
|
||||
"email": "Nieuw e-mailadres",
|
||||
"errorWrongPassword": "Onjuist wachtwoord",
|
||||
"password": "Wachtwoord ter bevestiging"
|
||||
},
|
||||
"changeFallbackEmail": {
|
||||
"errorEmailRequired": "Een geldig e-mailadres is verplicht",
|
||||
@@ -626,7 +633,7 @@
|
||||
},
|
||||
"check": {
|
||||
"noop": "Cloudron backups zijn uitgeschakeld. Zorg ervoor dat deze server op een andere manier wordt geback-upt. Kijk op https://docs.cloudron.io/backups/#storage-providers voor meer informatie.",
|
||||
"sameDisk": "Cloudron backups staan momenteel op dezelfde schijf als deze Cloudron server. Dit is gevaarlijk en kan leiden tot gegevensverlies als de schijf defect raakt. Kijk op https://docs.cloudron.io/backups/#storage-providers hoe je backups op een externe locatie kan zetten."
|
||||
"sameDisk": "Backups staan momenteel op dezelfde schijf als Cloudron zelf. Als de disk volloopt met deze backups zal Cloudron niet meer werken. Een defecte disk kan ook leiden tot volledig gegevensverlies. Kijk op https://docs.cloudron.io/backups/#storage-providers hoe je backups op een externe locatie kan zetten."
|
||||
},
|
||||
"backupEdit": {
|
||||
"preserved": {
|
||||
@@ -650,7 +657,9 @@
|
||||
},
|
||||
"changeLogo": {
|
||||
"title": "Kies een Cloudron-afbeelding"
|
||||
}
|
||||
},
|
||||
"backgroundImage": "Inlogpagina achtergrond afbeelding",
|
||||
"clearBackgroundImage": "Leegmaken"
|
||||
},
|
||||
"emails": {
|
||||
"title": "E-mail",
|
||||
@@ -823,7 +832,13 @@
|
||||
"cloudflareDefaultProxyStatus": "Inschakelen proxy voor nieuwe DNS regels",
|
||||
"porkbunApikey": "API sleutel",
|
||||
"porkbunSecretapikey": "Geheime API sleutel",
|
||||
"bunnyAccessKey": "Bunny toegangssleutel"
|
||||
"bunnyAccessKey": "Bunny toegangssleutel",
|
||||
"dnsimpleAccessToken": "Toegangstoken",
|
||||
"ovhEndpoint": "Eindpunt",
|
||||
"ovhConsumerKey": "Consumer sleutel",
|
||||
"ovhAppKey": "Applicatie sleutel",
|
||||
"ovhAppSecret": "Applicatie geheim",
|
||||
"deSecToken": "deSEC Token"
|
||||
},
|
||||
"title": "Domeinen & Certificaten",
|
||||
"addDomain": "Domein toevoegen",
|
||||
@@ -956,14 +971,14 @@
|
||||
"resources": {
|
||||
"memory": {
|
||||
"title": "Geheugenlimiet",
|
||||
"description": "Cloudron wijst 50% van deze waarde toe als RAM en 50% als swap.",
|
||||
"description": "Maximum geheugen dat een app kan gebruiken",
|
||||
"resizeAction": "Grootte wijzigen",
|
||||
"error": "Kan geheugenlimiet niet instellen, probeer minder."
|
||||
},
|
||||
"cpu": {
|
||||
"setAction": "Vastleggen",
|
||||
"title": "CPU Shares",
|
||||
"description": "Percentage CPU-tijd wanneer het systeem zwaar wordt belast."
|
||||
"setAction": "Instellen",
|
||||
"title": "CPU Limiet",
|
||||
"description": "Maximum percentage CPU dat een app kan gebruiken"
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
@@ -1025,14 +1040,15 @@
|
||||
"checkForUpdatesAction": "Controleer op updates",
|
||||
"customAppUpdateInfo": "Er zijn geen updates beschikbaar voor deze maatwerk app",
|
||||
"updateAvailableAction": "Update beschikbaar",
|
||||
"repository": "Pakket Opslagplaats"
|
||||
"repository": "Pakket Opslagplaats",
|
||||
"installedAt": "Geïnstalleerd op"
|
||||
},
|
||||
"auto": {
|
||||
"title": "Automatische updates",
|
||||
"enabled": "Automatische updates zijn momenteel ingeschakeld.",
|
||||
"disabled": "Automatische updates zijn momenteel uitgeschakeld.",
|
||||
"disableAction": "Automatische updates uitschakelen",
|
||||
"enableAction": "Automatische updates inschakelen",
|
||||
"disableAction": "Uitschakelen",
|
||||
"enableAction": "Inschakelen",
|
||||
"description": "Cloudron controleert de App Store periodiek op updates. Als je dit uitschakelt zorg er dan voor dat je updates handmatig installeert."
|
||||
},
|
||||
"noUpdates": "Geen nieuwe updates beschikbaar"
|
||||
@@ -1206,17 +1222,23 @@
|
||||
"title": "Redis configuratie",
|
||||
"enable": "Configureer de app om Redis te gebruiken",
|
||||
"disable": "Redis uitschakelen"
|
||||
},
|
||||
"infoTabTitle": "Info",
|
||||
"info": {
|
||||
"notes": {
|
||||
"title": "Admin Notities"
|
||||
}
|
||||
}
|
||||
},
|
||||
"network": {
|
||||
"title": "Netwerk",
|
||||
"ip": {
|
||||
"title": "IP Adres",
|
||||
"title": "IPv4",
|
||||
"provider": "Aanbieder",
|
||||
"interface": "Naam netwerkinterface",
|
||||
"configure": "Configureer",
|
||||
"interfaceDescription": "Toon beschikbare apparaten op deze server met:",
|
||||
"description": "Cloudron gebruikt dit IP adres tijdens het instellen van DNS records.",
|
||||
"description": "Cloudron gebruikt dit IPv4 adres om de DNS records in te stellen.",
|
||||
"detected": "gedetecteerd",
|
||||
"address": "IP adres"
|
||||
},
|
||||
@@ -1236,7 +1258,7 @@
|
||||
"showLogsAction": "Toon logbestanden"
|
||||
},
|
||||
"configureIp": {
|
||||
"title": "Configureer IP aanbieder",
|
||||
"title": "Configureer IPv4 aanbieder",
|
||||
"providerGenericDescription": "Het publieke IP adres van deze server wordt automatisch gedetecteerd."
|
||||
},
|
||||
"ipv4": {
|
||||
@@ -1285,7 +1307,7 @@
|
||||
"cloudronId": "Cloudron ID",
|
||||
"subscriptionEndsAt": "Opgezegd en eindigt op",
|
||||
"subscriptionSetupAction": "Upgrade naar Premium",
|
||||
"subscriptionChangeAction": "Abonnement wijzigen",
|
||||
"subscriptionChangeAction": "Beheer abonnement",
|
||||
"subscriptionReactivateAction": "Abonnement heractiveren",
|
||||
"title": "Cloudron.io Account",
|
||||
"description": "Een Cloudron.io account wordt gebruikt voor toegang tot de App Store en om je abonnement te beheren.",
|
||||
@@ -1381,6 +1403,10 @@
|
||||
"disableAction": "SSH ondersteuningstoegang uitschakelen",
|
||||
"enableAction": "SSH ondersteuningstoegang inschakelen",
|
||||
"description": "Met het inschakelen van deze optie geeft je ondersteuningsmedewerkers toegang tot deze server middels SSH."
|
||||
},
|
||||
"help": {
|
||||
"title": "Hulp",
|
||||
"description": "Gebruik de volgende bronnen voor hulp en ondersteuning:\n* [Cloudron Forum]({{ forumLink }}) - Gebruik de Support en App specifieke categorieën voor vragen.\n* [Cloudron Docs & Knowledge Base]({{ docsLink }})\n* [Custom App Packaging & API]({{ packagingLink }})\n"
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
@@ -1405,7 +1431,19 @@
|
||||
"graphTitle": "Percentage",
|
||||
"graphSubtext": "Alleen apps die meer dan {{ threshold }} van de CPU gebruiken worden getoond"
|
||||
},
|
||||
"selectPeriodLabel": "Selecteer periode"
|
||||
"selectPeriodLabel": "Selecteer periode",
|
||||
"info": {
|
||||
"title": "Info",
|
||||
"vendor": "Leverancier",
|
||||
"memory": "Geheugen",
|
||||
"uptime": "Uptime",
|
||||
"activationTime": "Cloudron installatie tijd",
|
||||
"platformVersion": "Platform Versie",
|
||||
"product": "Product"
|
||||
},
|
||||
"graphs": {
|
||||
"title": "Grafieken"
|
||||
}
|
||||
},
|
||||
"eventlog": {
|
||||
"title": "Logboek",
|
||||
@@ -1472,7 +1510,8 @@
|
||||
"renameDialog": {
|
||||
"title": "Hernoem {{ fileName }}",
|
||||
"newName": "Nieuwe naam",
|
||||
"rename": "Hernoem"
|
||||
"rename": "Hernoem",
|
||||
"reallyOverwrite": "Een bestand met die naam bestaat al. Wil je het bestaande bestand overschrijven?"
|
||||
},
|
||||
"chownDialog": {
|
||||
"newOwner": "Nieuwe eigenaar",
|
||||
@@ -1675,7 +1714,7 @@
|
||||
"setupDnsInfo": "Gebruik deze optie om automatisch e-mail gerelateerde DNS records in te stellen. Het nu niet inschakelen kan handig zijn om eerst e-mail boxen aan te maken en <a href=\"{{ importEmailDocsLink }}\">e-mails te importeren</a> voor ingebruikname.",
|
||||
"enableAction": "Inschakelen",
|
||||
"description": "Hiermee wordt Cloudron zo geconfigureerd dat e-mails ontvangen worden voor <b>{{ domain }}</b>. In de documentatie staat beschreven welke <a href=\"{{ requiredPortsDocsLink }}\" target=\"_blank\">benodigde poorten</a> ingesteld dienen te worden voor Cloudron Email.",
|
||||
"cloudflareInfo": "Het domein <code>{{ adminDomain }}</code> wordt beheerd door Cloudflare. Zorg ervoor dat Cloudflare proxying uitgeschakeld is voor <code>{{ mailFqdn }}</code> en ingesteld is op <code>DNS only</code>. Dit is noodzakelijk omdat Cloudflare geen e-mail-proxy kan uitvoeren."
|
||||
"cloudflareInfo": "Het domein van de mailserver <code>{{ adminDomain }}</code> wordt beheerd door Cloudflare. Zorg ervoor dat Cloudflare proxying uitgeschakeld is voor <code>{{ mailFqdn }}</code> en ingesteld is op <code>DNS only</code>. Dit is noodzakelijk omdat Cloudflare geen e-mail-proxy kan uitvoeren."
|
||||
},
|
||||
"disableEmailDialog": {
|
||||
"title": "E-mail Server voor {{ domain }} uitschakelen?",
|
||||
@@ -1704,7 +1743,7 @@
|
||||
},
|
||||
"addMailinglistDialog": {
|
||||
"title": "Maillijst toevoegen",
|
||||
"members": "Lijst leden",
|
||||
"members": "Ledenlijst",
|
||||
"membersInfo": "Plaats meerdere e-mailadressen elk op een nieuwe regel",
|
||||
"membersOnlyCheckbox": "Het versturen van e-mail aan deze lijst beperken tot de leden",
|
||||
"name": "Naam"
|
||||
@@ -1752,7 +1791,8 @@
|
||||
"2faToken": "2FA Token (indien ingeschakeld)",
|
||||
"errorIncorrectCredentials": "Onjuiste gebruikersnaam of wachtwoord",
|
||||
"errorIncorrect2FAToken": "2FA token is niet geldig",
|
||||
"errorInternal": "Interne fout, probeer later opnieuw"
|
||||
"errorInternal": "Interne fout, probeer later opnieuw",
|
||||
"loginWith": "Login met Cloudron"
|
||||
},
|
||||
"passwordReset": {
|
||||
"title": "Wachtwoord herstellen",
|
||||
@@ -1812,7 +1852,11 @@
|
||||
"mountStatus": "Koppel status",
|
||||
"localDirectory": "Lokale map",
|
||||
"type": "Type",
|
||||
"remountActionTooltip": "Her-koppel Volume"
|
||||
"remountActionTooltip": "Her-koppel Volume",
|
||||
"editVolumeDialog": {
|
||||
"title": "Bewerk volume {{ name }}"
|
||||
},
|
||||
"editActionTooltip": "Bewerk Volume"
|
||||
},
|
||||
"lang": {
|
||||
"it": "Italiaans",
|
||||
@@ -1826,7 +1870,8 @@
|
||||
"es": "Spaans",
|
||||
"ru": "Russisch",
|
||||
"pt": "Portugees",
|
||||
"da": "Deens"
|
||||
"da": "Deens",
|
||||
"id": "Indonesisch"
|
||||
},
|
||||
"passwordResetEmail": {
|
||||
"subject": "[<%= cloudron %>] Wachtwoord herstellen",
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
{
|
||||
"apps": {
|
||||
"title": "As Minhas Aplicações",
|
||||
"noApps": {
|
||||
"description": "Que tal instalar algumas? Vê a <a href=\"{{ appStoreLink }}\">Loja de Aplicações</a>",
|
||||
"title": "Sem aplicações instaladas!"
|
||||
},
|
||||
"groupsFilterHeader": "Selecionar Grupo",
|
||||
"addApplinkAction": "Adicionar Applink",
|
||||
"noAccess": {
|
||||
"title": "Não tem acesso a nenhuma aplicação.",
|
||||
"description": "Assim que tiver, elas vão aparecer aqui."
|
||||
},
|
||||
"configActionTooltip": "Configurar",
|
||||
"logsActionTooltip": "Eventos",
|
||||
"infoActionTooltip": "Informação",
|
||||
"adminPageActionTooltip": "Página de Adminstração",
|
||||
"searchPlaceholder": "Pesquisar Aplicações",
|
||||
"stateFilterHeader": "Todos os Estados",
|
||||
"tagsFilterHeader": "Etiquetas: {{ tags }}",
|
||||
"tagsFilterHeaderAll": "Todas as Etiquetas",
|
||||
"domainsFilterHeader": "Todos os Domínios",
|
||||
"auth": {
|
||||
"sso": "Entrar com as credenciais Cloudron",
|
||||
"nosso": "Entrar com conta dedicada",
|
||||
"email": "Entrar com endereço de email"
|
||||
},
|
||||
"addAppAction": "Adicionar Aplicação",
|
||||
"addAppproxyAction": "Adicionar Appproxy",
|
||||
"filter": {
|
||||
"clearAll": "Limpar Tudo"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"displayName": "Nome de Apresentação",
|
||||
"rebootDialog": {
|
||||
"warning": "Reiniciar o servidor irá causar que todas as aplicações instaladas neste Cloudron fiquem indisponíveis temporariamente!",
|
||||
"description": "Utilize isto para aplicar atualizações de segurança ou se experienciar comportamento inesperado. Todas as aplicações e serviços em execução neste Cloudron vão iniciar automaticamente quando o reinício estiver completo.",
|
||||
"title": "Realmente reiniciar o servidor?",
|
||||
"rebootAction": "Reiniciar agora"
|
||||
},
|
||||
"offline": "O Cloudron está offline. A ligar novamente…",
|
||||
"dialog": {
|
||||
"cancel": "Cancelar",
|
||||
"save": "Guardar",
|
||||
"close": "Fechar",
|
||||
"no": "Não",
|
||||
"yes": "Sim"
|
||||
},
|
||||
"logout": "Terminar Sessão",
|
||||
"username": "Nome de Utilizador",
|
||||
"actions": "Ações",
|
||||
"table": {
|
||||
"date": "Data"
|
||||
},
|
||||
"pagination": {
|
||||
"next": "seguinte",
|
||||
"prev": "anterior",
|
||||
"perPageSelector": "Mostrar {{ n }} por página"
|
||||
},
|
||||
"action": {
|
||||
"reboot": "Reiniciar",
|
||||
"logs": "Eventos"
|
||||
},
|
||||
"clipboard": {
|
||||
"copied": "Copiado para a área de transferência",
|
||||
"clickToCopy": "Clique para copiar",
|
||||
"clickToCopyBackupId": "Clique para copiar o ID da cópia de segurança"
|
||||
},
|
||||
"searchPlaceholder": "Pesquisar",
|
||||
"multiselect": {
|
||||
"selected": "{{ n }} selecionados",
|
||||
"select": "Selecionar",
|
||||
"filterPlaceholder": "Escreva para filtrar opções"
|
||||
},
|
||||
"prettyDate": {
|
||||
"justNow": "agora mesmo",
|
||||
"yeserday": "Ontem",
|
||||
"minutesAgo": "{{ m }} minutos atrás",
|
||||
"hoursAgo": "{{ h }} horas atrás"
|
||||
},
|
||||
"navbar": {
|
||||
"users": "Utilizadores"
|
||||
},
|
||||
"disableAction": "Desativar",
|
||||
"enableAction": "Ativar",
|
||||
"statusEnabled": "Ativado",
|
||||
"statusDisabled": "Desativado"
|
||||
},
|
||||
"appstore": {
|
||||
"category": {
|
||||
"analytics": "Estatísticas",
|
||||
"game": "Jogos",
|
||||
"project": "Gestão de Projetos",
|
||||
"all": "Tudo",
|
||||
"popular": "Popular",
|
||||
"newApps": "Novas Aplicações",
|
||||
"chat": "Chat",
|
||||
"blog": "Blog",
|
||||
"document": "Documentos",
|
||||
"crm": "CRM",
|
||||
"forum": "Fórum",
|
||||
"gallery": "Galeria",
|
||||
"finance": "Finanças",
|
||||
"git": "Alojamento de Código",
|
||||
"email": "Email",
|
||||
"hosting": "Alojamento Web",
|
||||
"media": "Multimédia",
|
||||
"learning": "Aprendizagem",
|
||||
"notes": "Notas",
|
||||
"sync": "Sincronização de Ficheiros",
|
||||
"wiki": "Wiki",
|
||||
"vpn": "VPN",
|
||||
"federated": "Federados"
|
||||
},
|
||||
"installDialog": {
|
||||
"lastUpdated": "Última atualização a {{ date }}",
|
||||
"locationPlaceholder": "Deixe em branco para usar o domínio de raiz",
|
||||
"userManagementNone": "Esta aplicação tem a sua própria gestão de utilizadores. Esta definição determina se a aplicação está ou não visível no painel do utilizador.",
|
||||
"memoryRequirement": "Requere pelo menos {{ size }} de memória",
|
||||
"location": "Localização",
|
||||
"manualWarning": "Adicione um registo A manualmente para <b>{{ location }}</b> apontando para o endereço IP público deste Cloudron",
|
||||
"userManagement": "Gestão de utilizadores",
|
||||
"userManagementMailbox": "Todos os utilizadores com uma caixa de correio neste Cloudron têm acesso.",
|
||||
"userManagementLeaveToApp": "Deixar a gestão de utilizadores para a aplicação",
|
||||
"userManagementAllUsers": "Permitir todos os utilizadores deste Cloudron",
|
||||
"installAnywayAction": "Instalar na mesma",
|
||||
"doInstallAction": "Instalar {{ dnsOverwrite ? 'e sobrescrever DNS' : '' }}",
|
||||
"userManagementSelectUsers": "Apenas permitir os seguintes utilizadores e grupos",
|
||||
"errorUserManagementSelectAtLeastOne": "Selecione pelo menos um utilizador ou grupo",
|
||||
"users": "Utilizadores",
|
||||
"groups": "Grupos",
|
||||
"configuredForCloudronEmail": "Esta aplicação está pré-configurada para ser utilizada com o <a href=\"{{ emailDocsLink }}\" target=\"_blank\">Email do Cloudron</a>.",
|
||||
"lowOnResources": "Este Cloudron está baixo em recursos.",
|
||||
"pleaseUpgradeServer": "Por favor, atualize para um servidor com mais memória. Em alternativa, liberte recursos removendo aplicações que não utiliza.",
|
||||
"subscriptionRequired": "Para instalar mais aplicação, uma subscrição paga é necessária.",
|
||||
"setupSubscriptionAction": "Configurar Subscrição",
|
||||
"installAction": "Instalar",
|
||||
"cloudflarePortWarning": "O proxy do Cloudflare deve estar desativado para o domínio da aplicação para que possa aceder a esta porta",
|
||||
"titleAndVersion": "Esta aplicação inclui {{ title }} {{ version }}"
|
||||
},
|
||||
"title": "Loja de Aplicações",
|
||||
"searchPlaceholder": "Pesquise por alternativas como Github, Dropbox, Slack, Trello, …",
|
||||
"noAppsFound": "Nenhuma aplicação encontrada.",
|
||||
"appMissing": "Falta uma aplicação? Contacte-nos.",
|
||||
"unstable": "Instável",
|
||||
"appNotFoundDialog": {
|
||||
"description": "Não existe nenhuma aplicação <b>{{ appId }}</b> com a versão <b>{{ version }}</b>.",
|
||||
"title": "Aplicação não encontrada"
|
||||
},
|
||||
"accountDialog": {
|
||||
"titleSignUp": "Registar com Cloudron.io",
|
||||
"titleLogin": "Entrar com Cloudron.io"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,8 @@
|
||||
"auth": {
|
||||
"sso": "Войдите, используя учётную запись Cloudron",
|
||||
"email": "Войдите, используя email",
|
||||
"nosso": "Войдите, используя Вашу учётную запись"
|
||||
"nosso": "Войдите, используя Вашу учётную запись",
|
||||
"openid": "Войти с помощью Cloudron OpenID"
|
||||
},
|
||||
"noAccess": {
|
||||
"description": "После открытия доступа приложения отобразятся здесь.",
|
||||
@@ -29,6 +30,9 @@
|
||||
"addApplinkAction": "Добавить App Link",
|
||||
"filter": {
|
||||
"clearAll": "Очистить все"
|
||||
},
|
||||
"apps": {
|
||||
"count": "Всего приложений: {{ count }}"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
@@ -48,7 +52,8 @@
|
||||
"justNow": "только что",
|
||||
"yeserday": "Вчера",
|
||||
"minutesAgo": "{{ m }} минут назад",
|
||||
"hoursAgo": "{{ h }} часов назад"
|
||||
"hoursAgo": "{{ h }} часов назад",
|
||||
"never": "Никогда"
|
||||
},
|
||||
"logout": "Выйти",
|
||||
"dialog": {
|
||||
@@ -161,7 +166,10 @@
|
||||
"loginAction": "Логин",
|
||||
"switchToSignUpAction": "Ещё нет учётной записи? Зарегистрироваться",
|
||||
"createAccountAction": "Создать учётную запись",
|
||||
"switchToLoginAction": "Уже есть учётная запись? Войти"
|
||||
"switchToLoginAction": "Уже есть учётная запись? Войти",
|
||||
"setupWithTokenAction": "Настройка",
|
||||
"setupToken": "Настроить Токен",
|
||||
"titleToken": "Войти с Настроенным Токеном"
|
||||
},
|
||||
"title": "Магазин приложений",
|
||||
"noAppsFound": "Приложения не найдены.",
|
||||
@@ -195,8 +203,7 @@
|
||||
"invitationTooltip": "Пригласить пользователя",
|
||||
"setGhostTooltip": "Обезличить",
|
||||
"mailmanagerTooltip": "Этот пользователь может управлять другими пользователями и почтовыми ящиками",
|
||||
"count": "Всего пользователей: {{ count }}",
|
||||
"makeLocalTooltip": "Сделать пользователя локальным"
|
||||
"count": "Всего пользователей: {{ count }}"
|
||||
},
|
||||
"title": "Каталог пользователей",
|
||||
"newUserAction": "Новый пользователь",
|
||||
@@ -217,11 +224,10 @@
|
||||
"require2FAWarning": "Сперва настройте 2FA, чтобы иметь доступ к аккаунту в будущем."
|
||||
},
|
||||
"externalLdap": {
|
||||
"description": "Cloudron будет синхронизировать пользователей и группы с внешнего сервера LDAP или ActiveDirectory. Проверка пароля для аутентификации таких пользователей выполняется на внешнем сервере. Синхронизация не запускается автоматически, ее нужно активировать вручную.",
|
||||
"description": "Эта настройка будет сихронизировать и идентифицировать пользователй и группы из внешнего сервера LDAP или AcriveDirectory. Синхронизация запускается с периодичностью, но также может быть запущена вручную.",
|
||||
"bindPassword": "Привязать пароль (необязательно)",
|
||||
"bindUsername": "Привязать Уникальное имя (DN)/Имя пользователя (необязательно)",
|
||||
"title": "Подключиться к удалённому каталогу",
|
||||
"subscriptionRequired": "Данная функция доступна только в платной подписке.",
|
||||
"subscriptionRequiredAction": "Настроить подписку сейчас",
|
||||
"noopInfo": "LDAP аутентификация не настроена.",
|
||||
"provider": "Поставщик",
|
||||
@@ -234,14 +240,15 @@
|
||||
"groupBaseDn": "Групповой корневой элемент",
|
||||
"groupFilter": "Фильтр группы",
|
||||
"groupnameField": "Поле с именем группы",
|
||||
"auth": "Войти",
|
||||
"autocreateUsersOnLogin": "Автоматически создавать пользователей после их входа в Cloudron",
|
||||
"auth": "Авторизоваться",
|
||||
"autocreateUsersOnLogin": "Автоматически создавать пользователей после их входа",
|
||||
"showLogsAction": "Показать логи",
|
||||
"syncAction": "Синхронизировать",
|
||||
"configureAction": "Настроить",
|
||||
"errorSelfSignedCert": "Сервер использует недействительный или самоподписанный сертификат.",
|
||||
"providerOther": "Другое",
|
||||
"providerDisabled": "Отключить"
|
||||
"providerDisabled": "Отключить",
|
||||
"disableWarning": "Источник аутентификации будет сброшен до локальных паролей для всех активных пользователей."
|
||||
},
|
||||
"subscriptionDialog": {
|
||||
"title": "Требуется подписка",
|
||||
@@ -270,7 +277,9 @@
|
||||
"errorDisplayNameRequired": "Требуется имя",
|
||||
"activeCheckbox": "Пользователь активен",
|
||||
"fallbackEmailPlaceholder": "Необязательно. Если не указано, будет использоваться основной почтовый ящик",
|
||||
"displayNamePlaceholder": "Необязательно. Если не указано, пользователь может указать во время регистрации"
|
||||
"displayNamePlaceholder": "Необязательно. Если не указано, пользователь может указать во время регистрации",
|
||||
"external2FA": "Настройка 2FA осуществляется внешним ресурсом аутентификации",
|
||||
"ldapGroups": "Группы LDAP"
|
||||
},
|
||||
"deleteUserDialog": {
|
||||
"title": "Удалить пользователя {{ username }}",
|
||||
@@ -294,7 +303,7 @@
|
||||
"description": "Ссылка для сброса пароля отправлена на электронную почту {{ email }}:",
|
||||
"sendEmailLinkAction": "Отправить ссылку пользователю по электронной почте",
|
||||
"emailSent": "Отправлено",
|
||||
"newLinkAction": "Отправить ссылку для сброса пароля",
|
||||
"newLinkAction": "Отправить ссылку для сброса",
|
||||
"reset2FAAction": "Сбросить 2FA",
|
||||
"sendAction": "Отправить письмо",
|
||||
"descriptionLink": "Скопировать ссылку для сброса пароля",
|
||||
@@ -357,7 +366,7 @@
|
||||
"exposedLdap": {
|
||||
"title": "Сервер LDAP",
|
||||
"ipRestriction": {
|
||||
"description": "Сервер каталогов может быть ограничен для определённого круга IP адресов.",
|
||||
"description": "Ограничьте доступ к серверу каталогов только для определённого круга IP-адресов и диапазонов. Строки, начинающиеся с <code>#</code>, будут считаться комментарием.",
|
||||
"placeholder": "IP-адреса или подсети, разделённые строками",
|
||||
"label": "Ограничить доступ"
|
||||
},
|
||||
@@ -367,7 +376,8 @@
|
||||
"label": "Привязать пароль",
|
||||
"description": "Все запросы LDAP должны быть идентифицированы при помощи данного секрета и уникального имени пользователя (DN) <i>{{ userDN }}</i>",
|
||||
"url": "URL сервера"
|
||||
}
|
||||
},
|
||||
"cloudflarePortWarning": "Для доступа к LDAP серверу через домен панели управления проксирование Cloudflare должно быть выключено"
|
||||
},
|
||||
"userImportDialog": {
|
||||
"title": "Импорт пользователей",
|
||||
@@ -391,12 +401,6 @@
|
||||
"all": "Все пользователи",
|
||||
"active": "Активные пользователи",
|
||||
"inactive": "Неактивные пользователи"
|
||||
},
|
||||
"makeLocalDialog": {
|
||||
"title": "Установить этого пользователя локально",
|
||||
"description": "Данное действие перенесёт пользователя с внешней директории LDAP в Cloudron.",
|
||||
"warning": "Для создания локального пароля пользователя его прежний пароль будет сброшен.",
|
||||
"submitAction": "Сделать локальным"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
@@ -409,7 +413,7 @@
|
||||
"changePassword": {
|
||||
"currentPassword": "Текущий пароль",
|
||||
"errorPasswordInvalid": "Пароль должен быть не менее 8 и не более 265 символов",
|
||||
"title": "Изменить пароль",
|
||||
"title": "Изменить ваш пароль",
|
||||
"newPassword": "Новый пароль",
|
||||
"newPasswordRepeat": "Повторите новый пароль",
|
||||
"errorPasswordRequired": "Требуется пароль",
|
||||
@@ -463,7 +467,10 @@
|
||||
"changeEmail": {
|
||||
"title": "Изменить главный адрес электронной почты",
|
||||
"errorEmailInvalid": "Неверный адрес электронной почты",
|
||||
"errorEmailRequired": "Требуется действительный адрес электронной почты"
|
||||
"errorEmailRequired": "Требуется действительный адрес электронной почты",
|
||||
"email": "Новый адрес электронной почты",
|
||||
"password": "Пароль для подтверждения",
|
||||
"errorWrongPassword": "Неверный пароль"
|
||||
},
|
||||
"changeFallbackEmail": {
|
||||
"title": "Изменить пароль электронной почты восстановления",
|
||||
@@ -532,7 +539,8 @@
|
||||
"packageVersion": "Версия контейнера",
|
||||
"lastUpdated": "Обновлен",
|
||||
"checkForUpdatesAction": "Проверить обновления",
|
||||
"repository": "Репозиторий"
|
||||
"repository": "Репозиторий",
|
||||
"installedAt": "Установлено"
|
||||
},
|
||||
"auto": {
|
||||
"description": "Cloudron периодически проверяет Магазин приложений на наличие обновлений. Если Вы выключаете автоматические обновления, не забывайте применять их вручную.",
|
||||
@@ -631,12 +639,12 @@
|
||||
"title": "Лимит памяти",
|
||||
"error": "Не получилось установить лимит памяти, попробуйте меньшее значение.",
|
||||
"resizeAction": "Изменить",
|
||||
"description": "Cloudron выделяет 50% этого значения из оперативной памяти и 50% из swap."
|
||||
"description": "Максимальное количество ОЗУ, которое может использовать приложение."
|
||||
},
|
||||
"cpu": {
|
||||
"setAction": "Установить",
|
||||
"title": "Доля CPU",
|
||||
"description": "Процент времени CPU, когда система находится под нагрузкой."
|
||||
"setAction": "Масштабировать",
|
||||
"title": "Лимит CPU",
|
||||
"description": "Максимальный процент CPU, который может быть задействован в работе приложения"
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
@@ -850,6 +858,12 @@
|
||||
"title": "Настроить Redis",
|
||||
"enable": "Настроить использование Redis в приложении",
|
||||
"disable": "Отключить Redis"
|
||||
},
|
||||
"infoTabTitle": "Информация",
|
||||
"info": {
|
||||
"notes": {
|
||||
"title": "Заметки администратора"
|
||||
}
|
||||
}
|
||||
},
|
||||
"backups": {
|
||||
@@ -968,7 +982,7 @@
|
||||
},
|
||||
"check": {
|
||||
"noop": "Резервное копирование Cloudron выключено. Пожалуйста, убедитесь, что на сервере настроены альтернативные способы резервного копирования. Советуем ознакомиться с информацией по ссылке https://docs.cloudron.io/backups/#storage-providers .",
|
||||
"sameDisk": "Cloudron сохраняет резервные копии на том же диске, где находится он сам. Это опасно, и может привести к потере данных в случае ошибки диска. Советуем ознакомиться с информацией по ссылке https://docs.cloudron.io/backups/#storage-providers для выбора облачного поставщика."
|
||||
"sameDisk": "В настоящий момент резервные копии сохраняются на системный диск с установленным Cloudron. Обратите внимание, что при полном заполнении диска бэкапами, Cloudron прекратит свою работу. Также, в случае поломки диска, вы можете полностью потерять доступ к вашим данным и бэкапам. Советуем ознакомиться с документацией https://docs.cloudron.io/backups/#storage-providers для выбора внешнего хранилища бэкапов."
|
||||
},
|
||||
"backupEdit": {
|
||||
"title": "Редактировать резервную копию",
|
||||
@@ -976,7 +990,8 @@
|
||||
"preserved": {
|
||||
"description": "Хранить резервную копию, игнорируя политику хранения",
|
||||
"tooltip": "Также будет сохранена почта и {{ appsLength } резервных копий."
|
||||
}
|
||||
},
|
||||
"remotePath": "Удаленный путь"
|
||||
}
|
||||
},
|
||||
"branding": {
|
||||
@@ -991,7 +1006,9 @@
|
||||
},
|
||||
"changeLogo": {
|
||||
"title": "Выбрать изображение Cloudron"
|
||||
}
|
||||
},
|
||||
"backgroundImage": "Фоновое изображение экрана входа",
|
||||
"clearBackgroundImage": "Очистить"
|
||||
},
|
||||
"emails": {
|
||||
"title": "Электронная почта",
|
||||
@@ -1018,7 +1035,8 @@
|
||||
"acl": "Почтовый ACL (Access Control List)",
|
||||
"maxMailSize": "Максимальный размер письма",
|
||||
"solrFts": "Полный поиск по тексту (Solr)",
|
||||
"aclOverview": "{{ dnsblZonesCount }} DNSBL зон"
|
||||
"aclOverview": "{{ dnsblZonesCount }} DNSBL зон",
|
||||
"virtualAllMail": "Папка \"Вся почта\""
|
||||
},
|
||||
"eventlog": {
|
||||
"title": "Журнал событий электронной почты",
|
||||
@@ -1109,12 +1127,16 @@
|
||||
},
|
||||
"action": {
|
||||
"queue": "Очередь"
|
||||
},
|
||||
"changeVirtualAllMailDialog": {
|
||||
"title": "Папка \"Вся почта\"",
|
||||
"description": "Папка \"Вся почта\" содержит все электронные письма из вашего почтового ящика. Данная папка может быть полезна в том случае, когда ваш почтовый клиент не поддерживает рекурсивный поиск по папкам."
|
||||
}
|
||||
},
|
||||
"network": {
|
||||
"ip": {
|
||||
"title": "IP Адрес",
|
||||
"description": "Cloudron будет использовать данный IP адрес для настройки записей DNS.",
|
||||
"title": "IPv4",
|
||||
"description": "Cloudron будет использовать данный IPv4 адрес для настройки A записей DNS.",
|
||||
"provider": "Источник",
|
||||
"interface": "Имя сетевого интерфейса",
|
||||
"configure": "Настроить",
|
||||
@@ -1139,7 +1161,7 @@
|
||||
"showLogsAction": "Показать логи"
|
||||
},
|
||||
"configureIp": {
|
||||
"title": "Настроить источник IP",
|
||||
"title": "Настроить поставщика IPv4",
|
||||
"providerGenericDescription": "Публичный IP адрес сервера будет обнаружен автоматически."
|
||||
},
|
||||
"ipv4": {
|
||||
@@ -1191,7 +1213,7 @@
|
||||
"cloudronId": "Cloudron ID",
|
||||
"subscriptionEndsAt": "Отменена и завершена",
|
||||
"subscriptionSetupAction": "Обновить до Premium",
|
||||
"subscriptionChangeAction": "Изменить подписку",
|
||||
"subscriptionChangeAction": "Управление подпиской",
|
||||
"subscriptionReactivateAction": "Реактивировать подписку",
|
||||
"emailNotVerified": "Электронная почта не подтверждена"
|
||||
},
|
||||
@@ -1284,6 +1306,10 @@
|
||||
"enableAction": "Включить SSH доступ",
|
||||
"title": "Удалённая поддержка",
|
||||
"description": "Выберите эту опцию, чтобы позволить сотрудникам поддержки подключиться к Вашему серверу через SSH."
|
||||
},
|
||||
"help": {
|
||||
"title": "Помощь",
|
||||
"description": "Для поддержки и помощи, пожалуйста, воспользуйтесь следующими ресурсами:\n* [Форум Cloudron]({{ forumLink }}) - пожалуйста, задавайте вопросы в соответствующих темах Поддержки или конкретных приложений.\n* [Документация Cloudron & База знаний]({{ docsLink }})\n* [Создание сторонних приложений и API]({{ packagingLink }})\n"
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
@@ -1308,7 +1334,19 @@
|
||||
"graphTitle": "Процент",
|
||||
"graphSubtext": "Отображаются приложения, использующие более {{ threshold }} CPU"
|
||||
},
|
||||
"selectPeriodLabel": "Выберите период"
|
||||
"selectPeriodLabel": "Выберите период",
|
||||
"info": {
|
||||
"platformVersion": "Версия Платформы",
|
||||
"product": "Продукт",
|
||||
"vendor": "Поставщик",
|
||||
"memory": "Память",
|
||||
"uptime": "Аптайм",
|
||||
"activationTime": "Время создания Cloudron",
|
||||
"title": "Информация"
|
||||
},
|
||||
"graphs": {
|
||||
"title": "Графики"
|
||||
}
|
||||
},
|
||||
"eventlog": {
|
||||
"title": "Журнал",
|
||||
@@ -1389,7 +1427,13 @@
|
||||
"cloudflareDefaultProxyStatus": "Активировать прокси для новых DNS записей",
|
||||
"porkbunApikey": "API Ключ",
|
||||
"porkbunSecretapikey": "Secret API Ключ",
|
||||
"bunnyAccessKey": "Ключ доступа Bunny"
|
||||
"bunnyAccessKey": "Ключ доступа Bunny",
|
||||
"dnsimpleAccessToken": "Токен доступа",
|
||||
"ovhEndpoint": "Конечная точка",
|
||||
"ovhConsumerKey": "Ключ пользователя",
|
||||
"ovhAppKey": "Ключ приложения",
|
||||
"ovhAppSecret": "Секрет приложения",
|
||||
"deSecToken": "deSEC Токен"
|
||||
},
|
||||
"addDomain": "Добавить домен",
|
||||
"removeDialog": {
|
||||
@@ -1463,7 +1507,8 @@
|
||||
"renameDialog": {
|
||||
"newName": "Новое имя",
|
||||
"rename": "Переименовать",
|
||||
"title": "Переименовать {{ fileName }}"
|
||||
"title": "Переименовать {{ fileName }}",
|
||||
"reallyOverwrite": "Файл с таким именем уже существует. Хотите перезаписать его?"
|
||||
},
|
||||
"chownDialog": {
|
||||
"newOwner": "Новый владелец",
|
||||
@@ -1586,8 +1631,8 @@
|
||||
"title": "Включить электронную почту для {{ domain }}?",
|
||||
"setupDnsCheckbox": "Установить почтовые DNS записи",
|
||||
"enableAction": "Включить",
|
||||
"description": "Данный параметр настроит Cloudron на получение писем для <b>{{ domain }}</b>. Прости ознакомиться с <a href=\"{{ requiredPortsDocsLink }}\" target=\"_blank\">документацией</a> для открытия необходимых почтовому серверу портов.",
|
||||
"cloudflareInfo": "Домен <code>{{ adminDomain }}</code> управляется при помощи Cloudflare. Пожалуйста, удостоверьтесь, что проксирование для <code>{{ mailFqdn}}</code> отключено, и активен только режим <code>DNS</code>. Это необходимо, так как Cloudflare не проксирует электронную почту.",
|
||||
"description": "Данный параметр настроит Cloudron на получение писем для <b>{{ domain }}</b>. Рекомендуем ознакомиться с <a href=\"{{ requiredPortsDocsLink }}\" target=\"_blank\">документацией</a> для открытия необходимых почтовому серверу портов.",
|
||||
"cloudflareInfo": "Почтовый домен <code>{{ adminDomain }}</code> управляется при помощи Cloudflare. Пожалуйста, удостоверьтесь, что проксирование для <code>{{ mailFqdn}}</code> отключено, и активен режим <code>только DNS</code>. Это необходимо, так как Cloudflare не проксирует электронную почту.",
|
||||
"setupDnsInfo": "Используйте данную опцию, чтобы автоматически настроить относящиеся к электронной почте записи DNS. Вы можете не отмечать её сразу, чтобы предварительно создать почтовые ящики и <a href=\"{{ importEmailDocsLink }}\">импортировать письма</a>."
|
||||
},
|
||||
"backAction": "Вернуться к электронной почте",
|
||||
@@ -1746,7 +1791,8 @@
|
||||
"2faToken": "2FA Токен (если включен)",
|
||||
"errorIncorrectCredentials": "Неправильное имя пользователя или пароль",
|
||||
"errorIncorrect2FAToken": "Неверный 2FA токен",
|
||||
"errorInternal": "Внутренняя ошибка, попробуйте позже"
|
||||
"errorInternal": "Внутренняя ошибка, попробуйте позже",
|
||||
"loginWith": "Войти с Cloudron"
|
||||
},
|
||||
"passwordReset": {
|
||||
"title": "Сброс пароля",
|
||||
@@ -1806,7 +1852,11 @@
|
||||
"title": "Тома",
|
||||
"hostPath": "Назначение",
|
||||
"description": "Тома - локальные или удаленные файловые системы. Они могут быть использованы для хранения данных приложений или для создания общей директории для нескольких приложений.",
|
||||
"localDirectory": "Локальный каталог"
|
||||
"localDirectory": "Локальный каталог",
|
||||
"editVolumeDialog": {
|
||||
"title": "Редактирование тома {{ name }}"
|
||||
},
|
||||
"editActionTooltip": "Редактировать том"
|
||||
},
|
||||
"lang": {
|
||||
"en": "Английский",
|
||||
|
||||
@@ -18,12 +18,18 @@
|
||||
"title": "Chưa có app cài đặt!",
|
||||
"description": "Cài đặt một vài app nhé? Hãy xem trong <a href=\"{{ appStoreLink }}\">Cửa hàng App</a>"
|
||||
},
|
||||
"groupsFilterHeader": "Chọn nhóm",
|
||||
"groupsFilterHeader": "Tất cả Nhóm",
|
||||
"auth": {
|
||||
"email": "Đăng nhập bằng email",
|
||||
"sso": "Đăng nhập với tên & mật khẩu trên Cloudron",
|
||||
"nosso": "Đăng nhập vào tài khoản riêng"
|
||||
}
|
||||
},
|
||||
"addAppAction": "Thêm App",
|
||||
"addApplinkAction": "Thêm đường link App",
|
||||
"filter": {
|
||||
"clearAll": "Xoá tất cả"
|
||||
},
|
||||
"addAppproxyAction": "Thêm proxy cho app"
|
||||
},
|
||||
"main": {
|
||||
"logout": "Thoát",
|
||||
@@ -32,7 +38,8 @@
|
||||
"save": "Lưu",
|
||||
"close": "Đóng",
|
||||
"no": "Không",
|
||||
"yes": "Có"
|
||||
"yes": "Có",
|
||||
"delete": "Xoá"
|
||||
},
|
||||
"username": "Tên đăng nhập",
|
||||
"displayName": "Tên hiển thị",
|
||||
@@ -42,11 +49,13 @@
|
||||
"pagination": {
|
||||
"prev": "trước",
|
||||
"next": "tiếp",
|
||||
"perPageSelector": "Hiển thị {{ n }} trên một trang"
|
||||
"perPageSelector": "Hiển thị {{ n }} trên một trang",
|
||||
"itemCount": "Đã tìm thấy {{ count }}"
|
||||
},
|
||||
"action": {
|
||||
"reboot": "Khởi động lại",
|
||||
"logs": "Log"
|
||||
"logs": "Log",
|
||||
"showLogs": "Hiển thị log"
|
||||
},
|
||||
"clipboard": {
|
||||
"clickToCopy": "Bấm để copy",
|
||||
@@ -79,7 +88,10 @@
|
||||
"users": "Người dùng"
|
||||
},
|
||||
"enableAction": "Bật",
|
||||
"disableAction": "Tắt"
|
||||
"disableAction": "Tắt",
|
||||
"loadingPlaceholder": "Đang tải",
|
||||
"settings": "Cài đặt",
|
||||
"saveAction": "Lưu"
|
||||
},
|
||||
"appstore": {
|
||||
"title": "Cửa hàng App",
|
||||
@@ -134,7 +146,8 @@
|
||||
"configuredForCloudronEmail": "App này đã được cấu hình sẵn để sử dụng với <a href=\"{{ emailDocsLink }}\" target=\"_blank\">Cloudron Email</a>.",
|
||||
"doInstallAction": "Tải về {{ dnsOverwrite ? 'and overwrite DNS' : '' }}",
|
||||
"cloudflarePortWarning": "Cần tắt proxy Cloudflare để tên miền app này có thể truy cập được vào cổng",
|
||||
"titleAndVersion": "App này đóng gói phần mềm {{ title }} {{ version }}"
|
||||
"titleAndVersion": "App này đóng gói phần mềm {{ title }} {{ version }}",
|
||||
"portReadOnly": "chỉ-đọc"
|
||||
},
|
||||
"appNotFoundDialog": {
|
||||
"title": "Không tìm thấy app",
|
||||
@@ -226,7 +239,6 @@
|
||||
"subscriptionRequiredAction": "Cài đặt gói đăng ký ngay",
|
||||
"description": "Cloudron sẽ đồng bộ người dùng và nhóm từ server LDAP hay ActiveDirectory bên ngoài. Xác minh mật khẩu cho người dùng được dựa trên server ngoài. Việc đồng bộ hoá không được chạy tự động mà cần được khởi động bằng tay.",
|
||||
"title": "Kết nối thư mục ngoài",
|
||||
"subscriptionRequired": "Tính năng này chỉ có trong gói trả phí.",
|
||||
"providerOther": "Khác",
|
||||
"providerDisabled": "Đã tắt"
|
||||
},
|
||||
@@ -247,8 +259,7 @@
|
||||
"invitationTooltip": "Mời Người dùng",
|
||||
"setGhostTooltip": "Nhập vai",
|
||||
"count": "Tổng ng dùng: {{ count }}",
|
||||
"mailmanagerTooltip": "Người dùng này có thể quản lý những ng dùng khác và cả những hộp thư",
|
||||
"makeLocalTooltip": "Người dùng địa phương"
|
||||
"mailmanagerTooltip": "Người dùng này có thể quản lý những ng dùng khác và cả những hộp thư"
|
||||
},
|
||||
"settings": {
|
||||
"saveAction": "Lưu",
|
||||
@@ -256,7 +267,7 @@
|
||||
"subscriptionRequired": "Chức năng này chỉ có trong gói trả phí.",
|
||||
"require2FACheckbox": "Yêu cầu người dùng cài đặt Mã xác minh 2 bước",
|
||||
"allowProfileEditCheckbox": "Cho phép người dùng chỉnh sửa tên và email",
|
||||
"title": "Cài đặt",
|
||||
"title": "Cài đặt Người dùng",
|
||||
"require2FAWarning": "Hãy cài đặt Mã xác minh 2 Bước cho tài khoản của bạn trước đề phòng bị khoá ra khỏi TK."
|
||||
},
|
||||
"groups": {
|
||||
@@ -328,8 +339,9 @@
|
||||
"label": "Giới hạn quyền truy cập"
|
||||
},
|
||||
"secret": {
|
||||
"label": "Mã bí mật",
|
||||
"description": "Tất cả những yêu cầu LDAP cần phải được xác minh với mã bí mật này và tên người dùng user DN <i>{{ userDN }}</i>"
|
||||
"label": "Mật khẩu bind",
|
||||
"description": "Tất cả những yêu cầu LDAP cần phải được xác minh với mã bí mật này và tên người dùng user DN <i>{{ userDN }}</i>",
|
||||
"url": "URL máy chủ"
|
||||
}
|
||||
},
|
||||
"userImportDialog": {
|
||||
@@ -352,12 +364,6 @@
|
||||
"all": "Tất cả Người dùng",
|
||||
"active": "Những người dùng đang hoạt động"
|
||||
},
|
||||
"makeLocalDialog": {
|
||||
"description": "Chức năng này sẽ di chuyển người dùng từ chỉ mục ngoài vào trong Cloudron.",
|
||||
"title": "Người dùng địa phương",
|
||||
"warning": "Phần đặt lại mật khẩu sẽ được kích hoạt để đặt một mật khẩu địa phương cho người dùng này.",
|
||||
"submitAction": "Địa phương hoá"
|
||||
},
|
||||
"setGhostDialog": {
|
||||
"generatePassword": "Tạo mật khẩu",
|
||||
"title": "Tạo mật khẩu để nhập vai người dùng {{ username }}",
|
||||
@@ -435,7 +441,8 @@
|
||||
"description": "Mã API mới:",
|
||||
"copyNow": "Xin copy mã API này bây giờ. Nó sẽ không được hiển thị lại vì lý do an ninh.",
|
||||
"generateToken": "Tạo mã API",
|
||||
"name": "Tên cho mã API"
|
||||
"name": "Tên cho mã API",
|
||||
"access": "Truy cập API"
|
||||
},
|
||||
"enable2FAAction": "Bật xác minh hai bước",
|
||||
"primaryEmail": "Email chính",
|
||||
@@ -458,7 +465,10 @@
|
||||
"name": "Tên",
|
||||
"expiresAt": "Hết hiệu lực vào",
|
||||
"lastUsed": "Lần dùng cuối",
|
||||
"neverUsed": "chưa từng dùng"
|
||||
"neverUsed": "chưa từng dùng",
|
||||
"readonly": "Chỉ đọc",
|
||||
"scope": "Mức độ bao phủ",
|
||||
"readwrite": "Đọc và Ghi"
|
||||
},
|
||||
"loginTokens": {
|
||||
"title": "Mã đăng nhập",
|
||||
@@ -540,7 +550,7 @@
|
||||
"mountPoint": "Điểm mount",
|
||||
"noopNote": "Lựa chọn này sẽ làm hỏng tính năng sao lưu và khôi phục của Cloudron và chỉ nên dùng khi test hệ thống. Xin đảm bảo rằng server được sao lưu toàn bộ bằng những phương tiện khác.",
|
||||
"format": "Định dạng lưu trữ",
|
||||
"encryptedFilenames": "Mã hoá tên tập tin",
|
||||
"encryptedFilenames": "Tên tập tin đã mã hoá",
|
||||
"chown": "Hệ thống tập tin bên ngoài có hỗ trợ chown",
|
||||
"username": "Tên đăng nhập",
|
||||
"server": "IP hoặc hostname máy chủ",
|
||||
@@ -552,7 +562,8 @@
|
||||
"user": "Người dùng",
|
||||
"privateKey": "Mật mã riêng",
|
||||
"diskPath": "Đường dẫn đến ổ đĩa",
|
||||
"cifsSealSupport": "Dùng mã hoá SEAL. Cần SMB thấp nhất là phiên bản v3"
|
||||
"cifsSealSupport": "Dùng mã hoá SEAL. Cần SMB thấp nhất là phiên bản v3",
|
||||
"encryptFilenames": "Mã hoá tên tập tin"
|
||||
},
|
||||
"cleanupBackups": {
|
||||
"description": "Các bản sao lưu được dọn sạch tự động dựa trên thời gian lưu giữ. Thao tác này sẽ xoá ngay lập tức các bản sao lưu đang có.",
|
||||
@@ -624,7 +635,9 @@
|
||||
"password": "Mật khẩu",
|
||||
"username": "Tên đăng nhập",
|
||||
"errorIncorrectCredentials": "Không đúng tên đăng nhập hoặc mật khẩu",
|
||||
"loginTo": "Đăng nhập vào"
|
||||
"loginTo": "Đăng nhập vào",
|
||||
"errorIncorrect2FAToken": "Mã bảo mật 2 Bước không đúng",
|
||||
"errorInternal": "Lỗi nội bộ hệ thống, vui lòng thử lại sau"
|
||||
},
|
||||
"setupAccount": {
|
||||
"username": "Tên đăng nhập",
|
||||
@@ -763,7 +776,8 @@
|
||||
"noAliases": "Không có tên gọi khác nào được chỉnh.",
|
||||
"aliases": "Tên gọi khác",
|
||||
"owner": "Chủ hộp thư",
|
||||
"title": "Chỉnh sửa hộp thư {{ name }}@{{ domain }}"
|
||||
"title": "Chỉnh sửa hộp thư {{ name }}@{{ domain }}",
|
||||
"enableStorageQuota": "Bật giới hạn lưu trữ"
|
||||
},
|
||||
"addMailboxDialog": {
|
||||
"owner": "Chủ hộp thư",
|
||||
@@ -846,7 +860,8 @@
|
||||
},
|
||||
"dyndns": {
|
||||
"description": "Bật lựa chọn này để đồng bộ các bản ghi DNS với một địa chỉ IP thường xuyên thay đổi. Việc này hữu ích khi Cloudron chạy trên hệ thống mạng với địa chỉ IP hay thay đổi như kết nối mạng ở nhà.",
|
||||
"title": "DNS động"
|
||||
"title": "DNS động",
|
||||
"showLogsAction": "Hiển thị log"
|
||||
},
|
||||
"firewall": {
|
||||
"configure": {
|
||||
@@ -879,7 +894,13 @@
|
||||
},
|
||||
"configureIpv6": {
|
||||
"title": "Cài đặt nhà cung cấp IPv6"
|
||||
}
|
||||
},
|
||||
"trustedIps": {
|
||||
"summary": "{{ trustCount }} địa chỉ IP được tin tưởng",
|
||||
"description": "Những HTTP header từ những địa chỉ IP trùng khớp sẽ được chấp thuận cho qua",
|
||||
"title": "Thiết lập những địa chỉ IP đáng tin cậy"
|
||||
},
|
||||
"trustedIpRanges": "Địa chỉ IP & Vùng được tin cậy "
|
||||
},
|
||||
"emails": {
|
||||
"typeFilterHeader": "Tất cả sự kiện",
|
||||
@@ -914,7 +935,7 @@
|
||||
"locationPlaceholder": "Để trống để dùng tên miền gốc",
|
||||
"location": "Vị trí",
|
||||
"title": "Thay đổi vị trí đặt mail server",
|
||||
"description": "Cloudron sẽ thay đổi những giá trị DNS cần thiết cho tất cả tên miền và khởi động lại mail server. Những client nhận mail trên máy tính hay điện thoại cần được cài đặt lại để sử dụng vị trí mới này làm IMAP và SMTP server."
|
||||
"description": "Hành động này sẽ di chuyển server IMAP và SMTP đến vị trí được xác định."
|
||||
},
|
||||
"eventlog": {
|
||||
"searchPlaceholder": "Tìm kiếm",
|
||||
@@ -933,7 +954,10 @@
|
||||
"queued": "Xếp hàng",
|
||||
"outgoing": "Gửi mail ra",
|
||||
"incoming": "Nhận mail vào",
|
||||
"deferred": "Trì hoãn lại"
|
||||
"deferred": "Trì hoãn lại",
|
||||
"overQuotaInfo": "Hộp thư {{ mailbox }} đã đầy {{ quotaPercent }}%",
|
||||
"underQuotaInfo": "Hộp thư {{ mailbox }} đã rơi xuống còn {{ quotaPercent }}% của hạn mức",
|
||||
"quota": "Hạn mức hộp thư"
|
||||
},
|
||||
"empty": "Log sự kiện hiện đang trống.",
|
||||
"details": "Chi tiết",
|
||||
@@ -950,8 +974,8 @@
|
||||
"solrEnabled": "Đã bật",
|
||||
"solrDisabled": "Đã tắt",
|
||||
"changeDomainProgress": "Thay đổi tên miền email:",
|
||||
"spamFilterOverview": "{{ blacklistCount }} email có trong danh sách đen.",
|
||||
"location": "Nơi đặt mail server",
|
||||
"spamFilterOverview": "{{ blacklistCount }} email có trong danh sách bị chặn.",
|
||||
"location": "Nơi đặt máy chủ mail",
|
||||
"spamFilter": "Lọc spam",
|
||||
"maxMailSize": "Kích cỡ mail tối đa",
|
||||
"info": "Các cài đặt này áp dụng cho tất cả các tên miền.",
|
||||
@@ -981,6 +1005,19 @@
|
||||
"dnsblZonesInfo": "Địa chỉ IP đang muốn kết nối đến được dò tìm trong những danh sách IP bị chặn này",
|
||||
"dnsblZonesPlaceholder": "Tên vùng (ghi xuống dòng)",
|
||||
"title": "Đổi danh sách quản lý truy cập mail"
|
||||
},
|
||||
"queue": {
|
||||
"empty": "Danh sách mail chờ đang trống",
|
||||
"title": "Danh sách mail chờ gửi",
|
||||
"rcptTo": "Gửi cho",
|
||||
"mailFrom": "Đến từ",
|
||||
"details": "Chi tiết",
|
||||
"discardTooltip": "Bỏ qua",
|
||||
"queueTime": "Thời gian chờ",
|
||||
"resendTooltip": "Gửi lại ngay"
|
||||
},
|
||||
"action": {
|
||||
"queue": "Cho vào hàng chờ gửi sau"
|
||||
}
|
||||
},
|
||||
"branding": {
|
||||
@@ -1009,10 +1046,11 @@
|
||||
"selectPeriodLabel": "Chọn khoảng thời gian",
|
||||
"cpuUsage": {
|
||||
"graphTitle": "Phần trăm sử dụng",
|
||||
"title": "Dung lượng CPU"
|
||||
"title": "Dung lượng CPU",
|
||||
"graphSubtext": "Chỉ những app sử dụng hơn {{ threshold }} cpu mới được hiển thị"
|
||||
},
|
||||
"systemMemory": {
|
||||
"graphSubtext": "Các giá trị bộ nhớ riêng từng app không hiển thị chồng lên nhau",
|
||||
"graphSubtext": "Chỉ những app sử dụng hơn {{ threshold }} bộ nhớ mới được hiển thị",
|
||||
"title": "Bộ nhớ hệ thống"
|
||||
},
|
||||
"diskUsage": {
|
||||
@@ -1020,7 +1058,11 @@
|
||||
"diskContent": "Ổ đĩa {{ type }} này hiện chứa",
|
||||
"usageInfo": "Còn {{ available | prettyDiskSize }}</b> trống trong tổng <b>{{ size | prettyDiskSize }}</b>",
|
||||
"mountedAt": "{{ filesystem }} <small>được gắn ở</small> {{ mountpoint }}",
|
||||
"title": "Dung lượng ổ đĩa"
|
||||
"title": "Dung lượng ổ đĩa",
|
||||
"usedInfo": "{{ used }} đã dùng trong tổng {{ size }}",
|
||||
"volumeContent": "Ổ đĩa này thuộc volume <code>{{ name }}</code>",
|
||||
"uninstalledApp": "App đã xoá",
|
||||
"diskSpeed": "Tốc độ: {{ speed }} MB/s"
|
||||
},
|
||||
"title": "Hệ thống"
|
||||
},
|
||||
@@ -1175,7 +1217,8 @@
|
||||
"download": "Tải xuống",
|
||||
"extract": "Giải nén tại đây",
|
||||
"chown": "Đổi quyền sở hữu",
|
||||
"rename": "Đổi tên"
|
||||
"rename": "Đổi tên",
|
||||
"open": "Mở"
|
||||
},
|
||||
"name": "Tên",
|
||||
"symlink": "Liên kết symlink đến {{ target }}",
|
||||
@@ -1233,7 +1276,19 @@
|
||||
},
|
||||
"removeDialog": {
|
||||
"reallyDelete": "Chắc chắn xoá?"
|
||||
}
|
||||
},
|
||||
"uploader": {
|
||||
"exitWarning": "Vẫn đang tải lên. Bạn có chắc muốn đóng trang này?",
|
||||
"uploading": "Đang tải lên"
|
||||
},
|
||||
"textEditor": {
|
||||
"undo": "Hoàn tác",
|
||||
"redo": "Xóa hoàn tác",
|
||||
"save": "Lưu"
|
||||
},
|
||||
"extractionInProgress": "Đang giải nén",
|
||||
"pasteInProgress": "Đang dán",
|
||||
"deleteInProgress": "Đang xoá"
|
||||
},
|
||||
"terminal": {
|
||||
"contextmenu": {
|
||||
@@ -1265,7 +1320,9 @@
|
||||
"logs": {
|
||||
"download": "Tải xuống tất cả log",
|
||||
"clear": "Làm sạch phần xem log",
|
||||
"title": "Log"
|
||||
"title": "Log",
|
||||
"notFoundError": "Không có tác vụ hay app đó",
|
||||
"logsGoneError": "Tập tin log không được tìm thấy"
|
||||
},
|
||||
"notifications": {
|
||||
"clearAll": "Xoá hết",
|
||||
@@ -1323,7 +1380,11 @@
|
||||
"wellKnownDescription": "Những giá trị nhập vào này sẽ được dùng bởi Cloudron để phản hồi về những đường link <code>/.well-known/</code>. Lưu ý rằng một app cần được đang chạy cài đặt sẵn trên tên miền gốc <code>{{ domain }}</code> để tính năng này có thể hoạt động được. Xem phần <a href=\"{{docsLink}}\" target=\"_blank\">hướng dẫn sử dụng</a> để biết thêm thông tin.",
|
||||
"vultrToken": "Mật mã Vultr",
|
||||
"jitsiHostname": "Vị trí Jitsi",
|
||||
"hetznerToken": "Mật mã Hetzner"
|
||||
"hetznerToken": "Mật mã Hetzner",
|
||||
"cloudflareDefaultProxyStatus": "Bật tính năng proxy cho những bản ghi DNS mới",
|
||||
"porkbunSecretapikey": "Mã bí mật API",
|
||||
"bunnyAccessKey": "Mã truy cập Bunny",
|
||||
"porkbunApikey": "Key API"
|
||||
},
|
||||
"subscriptionRequired": {
|
||||
"description": "Để thêm tên miền, hãy đăng ký gói trả phí.",
|
||||
@@ -1358,7 +1419,8 @@
|
||||
"domainWellKnown": {
|
||||
"title": "Những vị trí Well-Known của {{ domain }}"
|
||||
},
|
||||
"tooltipWellKnown": "Cài đặt những vị trí Well-Known"
|
||||
"tooltipWellKnown": "Cài đặt những vị trí Well-Known",
|
||||
"count": "Tổng số tên miền: {{ count }}"
|
||||
},
|
||||
"app": {
|
||||
"appInfo": {
|
||||
@@ -1423,7 +1485,8 @@
|
||||
"time": "Tạo ra lúc",
|
||||
"packageVersion": "Phiên bản đóng gói",
|
||||
"description": "Bản sao lưu là những bản chụp snapshot hoàn chỉnh của app. Bạn có thể dùng các bản sao lưu để khôi phục hoặc nhân bản app này.",
|
||||
"title": "Bản sao lưu"
|
||||
"title": "Bản sao lưu",
|
||||
"downloadBackupTooltip": "Tải bản sao lưu"
|
||||
}
|
||||
},
|
||||
"updates": {
|
||||
@@ -1443,8 +1506,10 @@
|
||||
"packageVersion": "Phiên bản đóng gói",
|
||||
"appId": "ID của app",
|
||||
"description": "Tên app và phiên bản",
|
||||
"title": "Thông tin app"
|
||||
}
|
||||
"title": "Thông tin app",
|
||||
"repository": "Repo của bản đống gói"
|
||||
},
|
||||
"noUpdates": "Không có phiên bản mới"
|
||||
},
|
||||
"security": {
|
||||
"robots": {
|
||||
@@ -1456,7 +1521,8 @@
|
||||
"saveAction": "Lưu",
|
||||
"title": "Chính sách an ninh nội dung",
|
||||
"description": "Cài đặt lựa chọn này sẽ ghi chèn lên những CSP header gửi từ app này ra"
|
||||
}
|
||||
},
|
||||
"hstsPreload": "Bật HSTS preload cho trang web này và tất cả tên miền phụ"
|
||||
},
|
||||
"email": {
|
||||
"csp": {
|
||||
@@ -1490,7 +1556,10 @@
|
||||
"24h": "24 tiếng trước",
|
||||
"12h": "12 tiếng trước",
|
||||
"6h": "6 tiếng"
|
||||
}
|
||||
},
|
||||
"diskTitle": "Dung lượng ổ đĩa",
|
||||
"diskIOTotal": "tổng: đọc {{ read }} / ghi {{ write }}",
|
||||
"networkIOTotal": "tổng: vào {{ inbound }} / ra {{ outbound }}"
|
||||
},
|
||||
"storage": {
|
||||
"mounts": {
|
||||
@@ -1499,13 +1568,20 @@
|
||||
"noMounts": "Không có volume được gắn thêm.",
|
||||
"volume": "Volume",
|
||||
"title": "Thư mục mount thêm",
|
||||
"readOnly": "Chỉ cho phép đọc"
|
||||
"readOnly": "Chỉ cho phép đọc",
|
||||
"permissions": {
|
||||
"readOnly": "Chỉ cho phép đọc",
|
||||
"readWrite": "Đọc và ghi",
|
||||
"label": "Quyền cấp phép"
|
||||
}
|
||||
},
|
||||
"appdata": {
|
||||
"moveAction": "Chuyển dữ liệu",
|
||||
"dataDirPlaceholder": "Để trống để dùng giá trị mặc định của hệ thống",
|
||||
"description": "Nếu hệ thống đang chạy sắp hết dung lượng ổ đĩa, hãy dùng chức năng này để dời những dữ liệu của app sang qua <a href=\"/#/volumes\">volume</a>. Bất cứ dữ liệu nào trong đây đều được sao lưu như một phần trong tổng thể app.",
|
||||
"title": "Thư mục Dữ liệu"
|
||||
"title": "Thư mục Dữ liệu",
|
||||
"diskUsage": "App hiện đang dùng {{ size }} trong bộ lưu trữ (tính đến ngày{{ date }}).",
|
||||
"mountTypeWarning": "Hệ thống tập tin điểm cuối phải hỗ trợ quyền cấp phép và sở hữu cho tập tin để có thể di chuyển dữ liệu"
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
@@ -1614,7 +1690,7 @@
|
||||
"setupSubscriptionAction": "Cài đặt gói đăng ký",
|
||||
"skipBackupCheckbox": "Bỏ qua sao lưu",
|
||||
"subscriptionExpired": "Gói đăng ký Cloudron của bạn đã hết hạn. Xin cài đặt một gói đăng ký để cập nhật app.",
|
||||
"changelogHeader": "Những thay đổi trong phiên bản mới {{ version}}:",
|
||||
"changelogHeader": "Những thay đổi trong phiên bản dóng gói mới {{ version}}:",
|
||||
"unstableWarning": "Bản cập nhật này là phiên bản ra mắt sớm và chưa được ổn định. Xin lưu ý rủi ro khi cập nhật.",
|
||||
"title": "Cập nhật {{ app }}"
|
||||
},
|
||||
@@ -1622,7 +1698,8 @@
|
||||
"importAction": "Nhập vào",
|
||||
"uploadAction": "Tải lên cấu hình bản sao lưu",
|
||||
"description": "Những dữ liệu được tạo ra tính từ thời điểm này và lần sao lưu cuối cùng sẽ bị mất vĩnh viễn. Bạn nên tạo một bản sao lưu của những dữ liệu hiện tại trước khi thực hiện việc nhập vào.",
|
||||
"title": "Nhập bản sao lưu vào"
|
||||
"title": "Nhập bản sao lưu vào",
|
||||
"remotePath": "Đường dẫn bản sao lưu"
|
||||
},
|
||||
"repairDialog": {
|
||||
"retryAction": "Thử lại {{ task }}",
|
||||
@@ -1661,7 +1738,30 @@
|
||||
"eventlogTabTitle": "Log sự kiện",
|
||||
"sftpInfoAction": "Quyền truy cập SFPT",
|
||||
"cronTabTitle": "Tác vụ lặp lai cron",
|
||||
"forumUrlAction": "Cần trợ giúp? Hãy hỏi thử trên diễn đàn nhé"
|
||||
"forumUrlAction": "Cần trợ giúp? Hãy hỏi thử trên diễn đàn nhé",
|
||||
"servicesTabTitle": "Dịch vụ",
|
||||
"turn": {
|
||||
"title": "Cài đặt TURN",
|
||||
"enable": "Thiết lập app để sử dụng máy chủ TURN được cài sẵn",
|
||||
"disable": "Không thiết lập TURN cho app này. Các cài đặt TURN cho app được giữ nguyên. Bạn có thể tuỳ chỉnh thêm trong app."
|
||||
},
|
||||
"redis": {
|
||||
"title": "Thiết lập Redis",
|
||||
"enable": "Thiết lập app sử dụng Redis"
|
||||
},
|
||||
"addApplinkDialog": {
|
||||
"title": "Thêm link app bên ngoài"
|
||||
},
|
||||
"editApplinkDialog": {
|
||||
"deleteAction": "Xoá",
|
||||
"title": "Chỉnh sửa link app"
|
||||
},
|
||||
"applinks": {
|
||||
"clearIconDescription": "Hệ thống sẽ lấy favicon của app sau khi bạn bấm lưu.",
|
||||
"upstreamUri": "Đường dẫn bên ngoài",
|
||||
"label": "Nhãn",
|
||||
"clearIconAction": "Xoá biểu tượng"
|
||||
}
|
||||
},
|
||||
"volumes": {
|
||||
"name": "Tên volume",
|
||||
@@ -1688,7 +1788,7 @@
|
||||
},
|
||||
"removeVolumeActionTooltip": "Xoá volume",
|
||||
"openFileManagerActionTooltip": "Mở Quản lý tập tin",
|
||||
"hostPath": "Đường dẫn mount",
|
||||
"hostPath": "Điểm đến",
|
||||
"addVolumeAction": "Thêm volume",
|
||||
"updateVolumeDialog": {
|
||||
"title": "Cập nhật Volume {{ volume }}"
|
||||
@@ -1720,7 +1820,9 @@
|
||||
"de": "Tiếng Đức",
|
||||
"en": "Tiếng Anh",
|
||||
"es": "Tiếng Tây Ban Nha",
|
||||
"ru": "Tiếng Nga"
|
||||
"ru": "Tiếng Nga",
|
||||
"da": "Tiếng Đan Mạch",
|
||||
"pt": "Tiếng Bồ Đào Nha"
|
||||
},
|
||||
"passwordResetEmail": {
|
||||
"subject": "[<%= cloudron %>] Đặt lại mật khẩu",
|
||||
@@ -1767,5 +1869,43 @@
|
||||
"mounts": {
|
||||
"description": "Các app có thể truy cập vào <a href=\"/#/volumes\">những volume</a> được mount lên thông qua thư mục <code>/media/{volume name}</code>. Dữ liệu này không được bao gồm trong phần bản sao lưu của app."
|
||||
}
|
||||
}
|
||||
},
|
||||
"oidc": {
|
||||
"newClientDialog": {
|
||||
"title": "Thêm client",
|
||||
"description": "Thêm cài đặt client kết nối OpenID mới.",
|
||||
"createAction": "Tạo"
|
||||
},
|
||||
"client": {
|
||||
"loginRedirectUri": "Đường dẫn callback khi đăng nhập (viết cách ra bởi dấu phẩy nếu có nhiều hơn một)",
|
||||
"name": "Tên",
|
||||
"id": "ID client",
|
||||
"secret": "Mật khẩu client",
|
||||
"signingAlgorithm": "Thuật toán ký mã hoá",
|
||||
"logoutRedirectUri": "Đường dẫn callback khi đăng nhập (không bắt buộc)"
|
||||
},
|
||||
"description": "Cloudron có thể làm nhà cung cấp kết nối OpenID cho các app trong và ngoài hệ thống.",
|
||||
"clients": {
|
||||
"title": "Client",
|
||||
"newClient": "Thêm client mới",
|
||||
"empty": "Chưa có client"
|
||||
},
|
||||
"title": "Nhà cung cấp kết nối OpenID",
|
||||
"editClientDialog": {
|
||||
"title": "Chỉnh sửa client {{ client }}"
|
||||
},
|
||||
"deleteClientDialog": {
|
||||
"title": "Chắc chắn muốn xoá client {{ client }}?",
|
||||
"description": "Thao tác này sẽ ngắt kết nối tất cả app OpenID bên ngoài có trong Cloudron sử dụng ID client này."
|
||||
},
|
||||
"env": {
|
||||
"discoveryUrl": "Đường dẫn Tìm kiếm",
|
||||
"logoutUrl": "Đường dẫn đăng xuất",
|
||||
"profileEndpoint": "Điểm cuối hồ sơ",
|
||||
"keysEndpoint": "Điểm cuối mật mã",
|
||||
"authEndpoint": "Điểm cuối Auth",
|
||||
"tokenEndpoint": "Điểm cuối token"
|
||||
}
|
||||
},
|
||||
"automation": "Tự động hoá"
|
||||
}
|
||||
|
||||
@@ -405,7 +405,6 @@
|
||||
"empty": "没有用户",
|
||||
"resetPasswordTooltip": "重设密码",
|
||||
"transferOwnershipTooltip": "转让所有权",
|
||||
"makeLocalTooltip": "设为本地用户",
|
||||
"invitationTooltip": "邀请用户",
|
||||
"setGhostTooltip": "模拟该用户",
|
||||
"mailmanagerTooltip": "该用户可以管理用户和邮箱",
|
||||
@@ -429,7 +428,6 @@
|
||||
},
|
||||
"externalLdap": {
|
||||
"title": "连接外部用户目录",
|
||||
"subscriptionRequired": "这个功能仅在付费订阅后可用。",
|
||||
"subscriptionRequiredAction": "现在就设置订阅",
|
||||
"noopInfo": "LDAP 认证未配置。",
|
||||
"provider": "Provider",
|
||||
@@ -549,12 +547,6 @@
|
||||
"setPassword": "设置密码",
|
||||
"generatePassword": "生成密码"
|
||||
},
|
||||
"makeLocalDialog": {
|
||||
"title": "将该用户改为本地用户",
|
||||
"warning": "会为该用户触发一次密码重置来设置本地密码。",
|
||||
"description": "该操作将会将用户从外部用户目录迁移到 Cloudron。",
|
||||
"submitAction": "设为本地用户"
|
||||
},
|
||||
"exposedLdap": {
|
||||
"secret": {
|
||||
"label": "密钥",
|
||||
|
||||
+212
-115
@@ -12,12 +12,47 @@
|
||||
<div ng-bind-html="app.manifest.postInstallMessage | markdown2html"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="form-group pull-left" ng-show="postInstallMessage.openApp">
|
||||
<input type="checkbox" id="appPostInstallConfirmCheckbox" ng-model="postInstallMessage.confirmed">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<a class="btn btn-success" ng-href="{{ postInstallMessage.confirmed ? ('https://' + app.fqdn) : '' }}" target="_blank" ng-disabled="!postInstallMessage.confirmed" ng-click="postInstallMessage.submit()" ng-show="postInstallMessage.openApp">{{ 'app.appInfo.openAction' | tr:{ app: app.manifest.title } }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal postinstall confirm -->
|
||||
<div class="modal fade" id="appPostInstallConfirmModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<img ng-src="{{appPostInstallConfirm.app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-info-icon"/>
|
||||
<h5 class="app-info-title">
|
||||
{{ appPostInstallConfirm.app.manifest.title }}
|
||||
<span class="app-info-meta text-small">{{ 'app.appInfo.package' | tr }} <a ng-href="/#/appstore/{{appPostInstallConfirm.app.manifest.id}}?version={{appPostInstallConfirm.app.manifest.version}}">v{{ appPostInstallConfirm.app.manifest.version }}</a> </span>
|
||||
<br/>
|
||||
<span ng-show="appPostInstallConfirm.app.manifest.documentationUrl"><a target="_blank" ng-href="{{appPostInstallConfirm.app.manifest.documentationUrl}}">{{ 'app.docsAction' | tr }}</a> </span>
|
||||
<br/>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-repeat="item in appPostInstallConfirm.app.checklist">
|
||||
<div class="checklist-item" ng-hide="item.acknowledged">
|
||||
{{ item.message }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p ng-show="appPostInstallConfirm.app.manifest.addons.email">{{ 'app.appInfo.ssoEmail' | tr }}</p>
|
||||
<p ng-show="appPostInstallConfirm.app.sso && !appPostInstallConfirm.app.manifest.addons.email">{{ 'app.appInfo.sso' | tr }}</p>
|
||||
|
||||
<div ng-bind-html="appPostInstallConfirm.app.manifest.postInstallMessage | markdown2html"></div>
|
||||
<div ng-show="appPostInstallConfirm.app.manifest.documentationUrl" ng-bind-html="'app.appInfo.appDocsUrl' | tr:{ docsUrl: appPostInstallConfirm.app.manifest.documentationUrl, title: appPostInstallConfirm.app.manifest.title, forumUrl: (appPostInstallConfirm.app.manifest.forumUrl || 'https://forum.cloudron.io') }"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="form-group pull-left">
|
||||
<input type="checkbox" id="appPostInstallConfirmCheckbox" ng-model="appPostInstallConfirm.confirmed">
|
||||
<label class="control-label" for="appPostInstallConfirmCheckbox">{{ 'app.appInfo.postInstallConfirmCheckbox' | tr }}</label>
|
||||
</div>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<a class="btn btn-success" ng-href="{{ postInstallMessage.confirmed ? ('https://' + app.fqdn) : '' }}" target="_blank" ng-disabled="!postInstallMessage.confirmed" ng-click="postInstallMessage.submit()" ng-show="postInstallMessage.openApp">{{ 'app.appInfo.openAction' | tr:{ app: app.manifest.title } }}</a>
|
||||
<a class="btn btn-success" ng-href="{{ appPostInstallConfirm.confirmed ? ('https://' + appPostInstallConfirm.app.fqdn) : '' }}" target="_blank" ng-disabled="!appPostInstallConfirm.confirmed" ng-click="appPostInstallConfirm.submit()">{{ 'app.appInfo.openAction' | tr:{ app: appPostInstallConfirm.app.manifest.title } }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -28,7 +63,7 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4>{{ 'app.accessControl.sftp.title' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#ftp-access" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></h4>
|
||||
<h4>{{ 'app.accessControl.sftp.title' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#sftp-access" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-small text-warning text-bold" ng-show="location.domain.provider === 'cloudflare'">{{ 'appstore.installDialog.cloudflarePortWarning' | tr }} </p>
|
||||
@@ -300,32 +335,45 @@
|
||||
|
||||
<!-- mountpoint -->
|
||||
<div class="form-group" ng-class="{ 'has-error': importBackup.error.mountPoint }" ng-show="importBackup.provider === 'mountpoint'">
|
||||
<label class="control-label" for="inputConfigureMountPoint">{{ 'backups.configureBackupStorage.mountPoint' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="importBackup.mountPoint" id="inputConfigureMountPoint" name="mountPoint" ng-disabled="importBackup.busy" placeholder="Folder where filesystem is mounted" ng-required="importBackup.provider === 'mountpoint'">
|
||||
<label class="control-label" for="inputImportMountPoint">{{ 'backups.configureBackupStorage.mountPoint' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="importBackup.mountPoint" id="inputImportMountPoint" name="mountPoint" ng-disabled="importBackup.busy" placeholder="Folder where filesystem is mounted" ng-required="importBackup.provider === 'mountpoint'">
|
||||
</div>
|
||||
|
||||
<!-- S3/Minio/SOS/GCS/SSHFS/CIFS/NFS/B2/Mountpoint -->
|
||||
<div class="form-group" ng-class="{ 'has-error': importBackup.error.prefix }" ng-show="importBackup.provider !== 'filesystem'">
|
||||
<label class="control-label" for="inputImportBackupPrefix">{{ 'backups.configureBackupStorage.prefix' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="importBackup.prefix" id="inputImportBackupPrefix" name="prefix" ng-disabled="importBackup.busy" placeholder="Prefix for backup file names">
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
<div class="form-group" ng-show="importBackup.provider === 'cifs' || importBackup.provider === 'nfs' || importBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupHost">{{ 'backups.configureBackupStorage.server' | tr }} ({{ importBackup.provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="importBackup.mountOptions.host" id="configureBackupHost" name="host" ng-disabled="importBackup.busy" placeholder="Server IP or hostname" ng-required="importBackup.provider === 'cifs' || importBackup.provider === 'nfs' || importBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="importBackupHost">{{ 'backups.configureBackupStorage.server' | tr }} ({{ importBackup.provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="importBackup.mountOptions.host" id="importBackupHost" name="host" ng-disabled="importBackup.busy" placeholder="Server IP or hostname" ng-required="importBackup.provider === 'cifs' || importBackup.provider === 'nfs' || importBackup.provider === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="importBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupPort">{{ 'backups.configureBackupStorage.port' | tr }}</label>
|
||||
<input type="number" class="form-control" ng-model="importBackup.mountOptions.port" id="configureBackupPort" name="port" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="importBackupPort">{{ 'backups.configureBackupStorage.port' | tr }}</label>
|
||||
<input type="number" class="form-control" ng-model="importBackup.mountOptions.port" id="importBackupPort" name="port" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="checkbox" ng-show="importBackup.provider === 'cifs'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="importBackup.mountOptions.seal">{{ 'backups.configureBackupStorage.cifsSealSupport' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
<div class="form-group" ng-show="importBackup.provider === 'cifs' || importBackup.provider === 'nfs' || importBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupRemoteDir">{{ 'backups.configureBackupStorage.remoteDirectory' | tr }} ({{ importBackup.provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="importBackup.mountOptions.remoteDir" id="configureBackupRemoteDir" name="remoteDir" ng-disabled="importBackup.busy" placeholder="/share" ng-required="importBackup.provider === 'cifs' || importBackup.provider === 'nfs' || importBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="importBackupRemoteDir">{{ 'backups.configureBackupStorage.remoteDirectory' | tr }} ({{ importBackup.provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="importBackup.mountOptions.remoteDir" id="importBackupRemoteDir" name="remoteDir" ng-disabled="importBackup.busy" placeholder="/share" ng-required="importBackup.provider === 'cifs' || importBackup.provider === 'nfs' || importBackup.provider === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<!-- EXT4/XFS -->
|
||||
<div class="form-group" ng-show="importBackup.provider === 'ext4' || importBackup.provider === 'xfs'" ng-class="{ 'has-error': importBackup.error.diskPath }">
|
||||
<label class="control-label" for="configureBackupDiskPath">{{ 'backups.configureBackupStorage.diskPath' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="importBackup.mountOptions.diskPath" id="configureBackupDiskPath" name="diskPath" ng-disabled="importBackup.busy" placeholder="/dev/disk/by-uuid/uuid" ng-required="importBackup.provider === 'ext4' || importBackup.provider === 'xfs'">
|
||||
<label class="control-label" for="importBackupDiskPath">{{ 'backups.configureBackupStorage.diskPath' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="importBackup.mountOptions.diskPath" id="importBackupDiskPath" name="diskPath" ng-disabled="importBackup.busy" placeholder="/dev/disk/by-uuid/uuid" ng-required="importBackup.provider === 'ext4' || importBackup.provider === 'xfs'">
|
||||
</div>
|
||||
|
||||
<!-- remotePath contains the prefix as well -->
|
||||
@@ -336,26 +384,26 @@
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="form-group" ng-show="importBackup.provider === 'cifs'">
|
||||
<label class="control-label" for="configureBackupUsername">{{ 'backups.configureBackupStorage.username' | tr }} ({{ importBackup.provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="importBackup.mountOptions.username" id="configureBackupUsername" name="username" ng-disabled="importBackup.busy">
|
||||
<label class="control-label" for="importBackupUsername">{{ 'backups.configureBackupStorage.username' | tr }} ({{ importBackup.provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="importBackup.mountOptions.username" id="importBackupUsername" name="username" ng-disabled="importBackup.busy">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="form-group" ng-show="importBackup.provider === 'cifs'">
|
||||
<label class="control-label" for="configureBackupPassword">{{ 'backups.configureBackupStorage.password' | tr }} ({{ importBackup.provider }})</label>
|
||||
<input type="password" class="form-control" ng-model="importBackup.mountOptions.password" id="configureBackupPassword" name="password" ng-disabled="importBackup.busy" password-reveal>
|
||||
<label class="control-label" for="importBackupPassword">{{ 'backups.configureBackupStorage.password' | tr }} ({{ importBackup.provider }})</label>
|
||||
<input type="password" class="form-control" ng-model="importBackup.mountOptions.password" id="importBackupPassword" name="password" ng-disabled="importBackup.busy" password-reveal>
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="importBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupUser">{{ 'backups.configureBackupStorage.user' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="importBackup.mountOptions.user" id="configureBackupUser" name="user" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="importBackupUser">{{ 'backups.configureBackupStorage.user' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="importBackup.mountOptions.user" id="importBackupUser" name="user" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="importBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupPrivateKey">{{ 'backups.configureBackupStorage.privateKey' | tr }}</label>
|
||||
<textarea class="form-control" ng-model="importBackup.mountOptions.privateKey" id="configureBackupPrivateKey" name="privateKey" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 'sshfs'"></textarea>
|
||||
<label class="control-label" for="importBackupPrivateKey">{{ 'backups.configureBackupStorage.privateKey' | tr }}</label>
|
||||
<textarea class="form-control" ng-model="importBackup.mountOptions.privateKey" id="importBackupPrivateKey" name="privateKey" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 'sshfs'"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': importBackup.error.region }" ng-show="importBackup.provider === 's3'">
|
||||
@@ -578,17 +626,17 @@
|
||||
<p class="text-small text-warning" ng-show="clone.domain.provider === 'noop' || clone.domain.provider === 'manual'" ng-bind-html="'appstore.installDialog.manualWarning' | tr:{ location: ((clone.subdomain ? clone.subdomain + '.' : '') + clone.domain.domain) }"></p>
|
||||
|
||||
<div class="has-error text-center" ng-show="clone.error.port">{{ clone.error.port }}</div>
|
||||
<div ng-repeat="(env, info) in clone.portBindingsInfo">
|
||||
<div ng-repeat="(env, info) in clone.portInfo">
|
||||
<ng-form name="portInfo_form">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!clone.itemName{{$index}}.$dirty && clone.error.port) || (portInfo_form.itemName{{$index}}.$dirty && portInfo_form.itemName{{$index}}.$invalid) }">
|
||||
<label class="control-label" for="inputPortInfo{{env}}"><input type="checkbox" ng-model="clone.portBindingsEnabled[env]">
|
||||
<label class="control-label" for="inputPortInfo{{env}}"><input type="checkbox" ng-model="clone.portsEnabled[env]">
|
||||
{{ info.title }}
|
||||
<sup>
|
||||
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})"><i class="fa fa-question-circle"></i></a>
|
||||
</sup>
|
||||
<small style="padding-left: 5px;" ng-show="info.readOnly">{{ 'appstore.installDialog.portReadOnly' | tr }}</small>
|
||||
</label>
|
||||
<input type="number" class="form-control" ng-model="clone.portBindings[env]" ng-disabled="!clone.portBindingsEnabled[env]" ng-readonly="info.readOnly" id="inputPortInfo{{env}}" later-name="itemName{{$index}}" min="{{hostPortMin}}" max="{{hostPortMax}}" required>
|
||||
<input type="number" class="form-control" ng-model="clone.ports[env]" ng-disabled="!clone.portsEnabled[env]" ng-readonly="info.readOnly" id="inputPortInfo{{env}}" later-name="itemName{{$index}}" min="{{hostPortMin}}" max="{{hostPortMax}}" required>
|
||||
<p class="text-small text-warning text-bold" ng-show="clone.domain.provider === 'cloudflare'">{{ 'appstore.installDialog.cloudflarePortWarning' | tr }} </p>
|
||||
</div>
|
||||
</ng-form>
|
||||
@@ -672,6 +720,7 @@
|
||||
<div class="row app-configure-links-container" ng-show="view">
|
||||
<div class="col-sm-2">
|
||||
<div class="app-configure-links">
|
||||
<div ng-click="setView('info')" ng-class="{ 'active': view === 'info' }">{{ 'app.infoTabTitle' | tr }}</div>
|
||||
<div ng-click="setView('display')" ng-class="{ 'active': view === 'display' }">{{ 'app.displayTabTitle' | tr }}</div>
|
||||
<div ng-click="setView('location')" ng-class="{ 'active': view === 'location' }" ng-show="app.accessLevel === 'admin'">{{ 'app.locationTabTitle' | tr }}</div>
|
||||
<div ng-click="setView('proxy')" ng-class="{ 'active': view === 'proxy' }" ng-show="app.type === APP_TYPES.PROXIED">Proxy</div>
|
||||
@@ -690,7 +739,101 @@
|
||||
<div ng-click="setView('uninstall')" ng-class="{ 'active': view === 'uninstall' }" ng-show="app.accessLevel === 'admin'">{{ 'app.uninstallTabTitle' | tr }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-8 card-container">
|
||||
<div class="card" ng-show="view === 'info'">
|
||||
<p>
|
||||
<label class="control-label">{{ 'app.updates.info.title' | tr }}</label>
|
||||
<a href="" class="pull-right" ng-click="info.showDoneChecklist = true" ng-show="info.hasOldChecklist && !info.showDoneChecklist">Show Checklist</a>
|
||||
<a href="" class="pull-right" ng-click="info.showDoneChecklist = false" ng-show="info.showDoneChecklist">Hide Checklist</a>
|
||||
</p>
|
||||
|
||||
<div ng-repeat="(key, item) in app.checklist">
|
||||
<div class="checklist-item" ng-hide="item.acknowledged">
|
||||
{{ item.message }}
|
||||
<button class="btn btn-xs btn-default" style="margin-left: 10px;" ng-click="info.checklistAck(item, key)">Done</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-repeat="(key, item) in app.checklist" ng-show="info.showDoneChecklist">
|
||||
<div class="checklist-item checklist-item-acknowledged" ng-show="item.acknowledged">
|
||||
{{ item.message }}<br/>
|
||||
<span class="text-muted text-small">{{ item.changedBy }} {{ item.changedAt | prettyDate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 10px"></div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-4">
|
||||
<span class="text-muted">{{ 'app.updates.info.description' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-8 text-right">
|
||||
<span ng-show="app.appStoreId">{{ app.manifest.title }} {{ app.upstreamVersion }}</span>
|
||||
<span ng-show="!app.appStoreId">{{ app.manifest.dockerImage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'app.updates.info.appId' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ app.id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'app.updates.info.packageVersion' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span ng-show="app.appStoreId"><a ng-href="/#/appstore/{{app.manifest.id}}?version={{app.manifest.version}}">{{ app.manifest.id }}@{{ app.manifest.version }}</a></span>
|
||||
<span ng-show="!app.appStoreId">{{ app.manifest.version }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'app.updates.info.installedAt' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ app.creationTime | prettyDate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'app.updates.info.lastUpdated' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ app.updateTime | prettyDate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<p><label class="control-label">{{ 'app.info.notes.title' | tr }}</label><i ng-show="!info.notes.editing" class="info-edit-indicator fa fa-pencil-alt" ng-click="info.notes.edit()"></i></p>
|
||||
<div class="row">
|
||||
<div class="col-md-12" ng-show="!info.notes.busy">
|
||||
<div ng-show="!info.notes.editing">
|
||||
<div ng-show="info.notes.content" ng-bind-html="info.notes.content | markdown2html"></div>
|
||||
<div ng-show="!info.notes.content" class="text-muted hand" ng-click="info.notes.edit()">{{ info.notes.placeholder }}</div>
|
||||
</div>
|
||||
<div ng-show="info.notes.editing" class="text-right">
|
||||
<textarea id="adminNotesTextarea" ng-trim="false" style="white-space: pre-wrap; margin-bottom: 5px" ng-model="info.notes.content" class="form-control" rows="10"></textarea>
|
||||
<button class="btn btn-default" ng-click="info.notes.dismiss()" ng-disabled="info.notes.busySave">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button class="btn btn-success" ng-click="info.notes.submit()" ng-disabled="info.notes.busySave"><i class="fa fa-circle-notch fa-spin" ng-show="info.notes.busySave"></i> {{ 'app.display.saveAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" ng-show="view === 'display'">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
@@ -711,7 +854,7 @@
|
||||
</div>
|
||||
<div id="previewIcon" class="app-custom-icon" ng-click="display.showCustomIconSelector()">
|
||||
<img ng-src="{{ display.iconUrl() || 'img/appicon_fallback.png' }}" fallback-icon="img/appicon_fallback.png" onerror="imageErrorHandler(this)"/>
|
||||
<div class="overlay"></div>
|
||||
<i class="picture-edit-indicator fa fa-pencil-alt"></i>
|
||||
</div>
|
||||
<a href="" style="font-weight: normal;" ng-click="display.resetCustomIcon()">{{ 'app.display.iconResetAction' | tr }}</a>
|
||||
<input type="file" id="iconFileInput" style="display: none" accept="image/png"/>
|
||||
@@ -724,8 +867,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="display.submit()" ng-disabled="(!display.icon.data && !displayForm.$dirty) || display.$invalid || display.busy"><i class="fa fa-circle-notch fa-spin" ng-show="display.busy"></i> {{ 'app.display.saveAction' | tr }}</button>
|
||||
</div>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="display.submit()" ng-disabled="(!display.icon.data && !displayForm.$dirty) || display.$invalid || display.busy"><i class="fa fa-circle-notch fa-spin" ng-show="display.busy"></i> {{ 'app.display.saveAction' | tr }}</button> </div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -790,17 +932,18 @@
|
||||
</div>
|
||||
|
||||
<div class="has-error text-center" ng-show="location.error.port">{{ location.error.port }}</div>
|
||||
<div ng-repeat="(env, info) in location.portBindingsInfo">
|
||||
<div ng-repeat="(env, info) in location.portInfo">
|
||||
<ng-form name="portInfo_form">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!portInfo_form.itemName{{$index}}.$dirty && location.error.port) || (portInfo_form.itemName{{$index}}.$dirty && portInfo_form.itemName{{$index}}.$invalid) }">
|
||||
<label class="control-label" for="locationPortInput{{env}}"><input type="checkbox" ng-model="location.portBindingsEnabled[env]">
|
||||
<label class="control-label" style="width: 100%" for="locationPortInput{{env}}"><input type="checkbox" ng-model="location.portsEnabled[env]">
|
||||
{{ info.title }}
|
||||
<sup>
|
||||
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})"><i class="fa fa-question-circle"></i></a>
|
||||
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}}. {{info.portCount >=1 ? (info.portCount + ' ports. ') : ''}} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})"><i class="fa fa-question-circle"></i></a>
|
||||
</sup>
|
||||
<small style="padding-left: 5px;" ng-show="info.readOnly">{{ 'appstore.installDialog.portReadOnly' | tr }}</small>
|
||||
<span ng-show="info.portCount" style="display: block; float: right">{{ location.ports[env] }} to {{ location.ports[env] + info.portCount - 1 }} ({{ info.portCount }} ports)</span>
|
||||
</label>
|
||||
<input type="number" class="form-control" ng-model="location.portBindings[env]" ng-disabled="!location.portBindingsEnabled[env]" ng-readonly="info.readOnly" id="locationPortInput{{env}}" later-name="itemName{{$index}}" min="{{HOST_PORT_MIN}}" max="{{HOST_PORT_MAX}}" required>
|
||||
<input type="number" class="form-control" ng-model="location.ports[env]" ng-disabled="!location.portsEnabled[env]" ng-readonly="info.readOnly" id="locationPortInput{{env}}" later-name="itemName{{$index}}" min="{{HOST_PORT_MIN}}" max="{{HOST_PORT_MAX}}" required>
|
||||
<p class="text-small text-warning text-bold" ng-show="location.domain.provider === 'cloudflare'">{{ 'appstore.installDialog.cloudflarePortWarning' | tr }} </p>
|
||||
</div>
|
||||
</ng-form>
|
||||
@@ -965,12 +1108,12 @@
|
||||
<form role="form" name="resourcesForm" ng-submit="resources.submitMemoryLimit()" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="memoryLimit">{{ 'app.resources.memory.title' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#memory-limit" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup> : <b>{{ resources.memoryLimit | prettyBinarySize:'Default (256 MiB)' }}</b></label>
|
||||
<div style="padding: 0 10px;">
|
||||
<slider id="memoryLimit" ng-model="resources.memoryLimit" step="134217728" tooltip="hide" ticks="resources.memoryTicks" ticks-snap-bounds="67108864"></slider>
|
||||
</div>
|
||||
<p>{{ 'app.resources.memory.description' | tr }}</p>
|
||||
<input type="range" id="memoryLimit" ng-model="resources.memoryLimit" step="134217728" min="{{ resources.memoryTicks[0] }}" max="{{ resources.memoryTicks[resources.memoryTicks.length-1] }}" list="memoryLimitTicks" />
|
||||
<datalist id="memoryLimitTicks">
|
||||
<option ng-repeat="limit in resources.memoryTicks" value="{{ limit }}"></option>
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="resources.memoryLimit === resources.currentMemoryLimit || resourcesForm.$invalid || resources.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -979,34 +1122,31 @@
|
||||
<span ng-show="resources.error.memoryLimit" class="text-danger">{{ 'app.resources.memory.error' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-md-4 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="resources.submitMemoryLimit()" ng-disabled="resources.memoryLimit === resources.currentMemoryLimit || resourcesForm.$invalid || resources.busy || app.error || app.taskId" tooltip-enable="app.error || app.taskId" uib-tooltip="{{ app.error ? 'App is in error state' : 'App is busy' }}">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="resources.busy"></i> {{ 'app.resources.memory.resizeAction' | tr }}
|
||||
</button>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="resources.submitMemoryLimit()" ng-disabled="resources.memoryLimit === resources.currentMemoryLimit || resourcesForm.$invalid || resources.busy || app.error || app.taskId" tooltip-enable="app.error || app.taskId" uib-tooltip="{{ app.error ? 'App is in error state' : 'App is busy' }}">{{ 'app.resources.memory.resizeAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form role="form" name="resourcesForm" ng-submit="resources.submitCpuShares()" autocomplete="off">
|
||||
<form role="form" name="resourcesForm" ng-submit="resources.submitCpuQuota()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="cpuShares">{{ 'app.resources.cpu.title' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#cpu-shares" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup> : <b>{{ (resources.cpuShares * 100 / 1024 | number:0) + ' %' }}</b></label>
|
||||
<label class="control-label" for="cpuQuota">{{ 'app.resources.cpu.title' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#cpu-quota" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup> : <b>{{ resources.cpuQuota + ' %' }}</b></label>
|
||||
<p>{{ 'app.resources.cpu.description' | tr }}</p>
|
||||
<div style="padding: 0 10px;">
|
||||
<slider id="cpuShares" ng-model="resources.cpuShares" ticks="[32, 256, 512, 768, 1024]" step="32" ticks-snap-bounds="32" min="32" max="1024" tooltip="hide"></slider>
|
||||
</div>
|
||||
<input type="range" id="cpuQuota" ng-model="resources.cpuQuota" step="1" min="1" max="100"/>
|
||||
<datalist id="cpuQuotaTicks">
|
||||
<option value="25"></option>
|
||||
<option value="50"></option>
|
||||
<option value="75"></option>
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="resources.cpuShares === resources.currentCpuShares || resourcesForm.$invalid || resources.busyCpuShares"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="resources.submitCpuShares()" ng-disabled="resources.cpuShares === resources.currentCpuShares || resourcesForm.$invalid || resources.busyCpuShares || app.error || app.taskId" tooltip-enable="app.error || app.taskId" uib-tooltip="{{ app.error ? 'App is in error state' : 'App is busy' }}">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="resources.busyCpuShares"></i> {{ 'app.resources.cpu.setAction' | tr }}
|
||||
</button>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="resources.submitCpuQuota()" ng-disabled="resources.cpuQuota === resources.currentCpuQuota || resourcesForm.$invalid || resources.busy || app.error || app.taskId" tooltip-enable="app.error || app.taskId" uib-tooltip="{{ app.error ? 'App is in error state' : 'App is busy' }}">{{ 'app.resources.cpu.setAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1326,7 +1466,8 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-md-2">{{ 'eventlog.time' | tr }}</th> <!-- "minutes ago" takes space -->
|
||||
<th class="col-md-8">{{ 'eventlog.details' | tr }}</th>
|
||||
<th class="col-md-2">{{ 'eventlog.source' | tr }}</th>
|
||||
<th class="col-md-6">{{ 'eventlog.details' | tr }}</th>
|
||||
<th class="col-md-2" style="text-align: right;">
|
||||
<button class="btn btn-xs btn-default btn-outline" ng-click="eventlog.showPrevPage()" ng-disabled="eventlog.busy || eventlog.currentPage <= 1"><i class="fa fa-angle-double-left"></i></button>
|
||||
<button class="btn btn-xs btn-default btn-outline" ng-click="eventlog.showNextPage()" ng-disabled="eventlog.busy || eventlog.perPage > eventlog.eventLogs.length"><i class="fa fa-angle-double-right"></i></button>
|
||||
@@ -1336,10 +1477,14 @@
|
||||
<tbody ng-repeat="eventLog in eventlog.eventLogs">
|
||||
<tr ng-click="eventlog.showDetails(eventLog)" class="hand">
|
||||
<td><span uib-tooltip="{{ eventLog.raw.creationTime | prettyLongDate }}" class="arrow">{{ eventLog.raw.creationTime | prettyDate }}</span></td>
|
||||
<td>{{ eventLog.source }}</td>
|
||||
<td style="word-wrap: anywhere;" colspan="2" ng-bind-html="eventLog.details"></td>
|
||||
</tr>
|
||||
<tr ng-show="eventlog.activeEventLog === eventLog">
|
||||
<td colspan="3"><pre class="eventlog-details">{{ eventLog.raw.data | json }}</pre></td>
|
||||
<td colspan="4">
|
||||
<p ng-show="eventLog.raw.source.ip">Source IP: <code>{{ eventLog.raw.source.ip }}</code></p>
|
||||
<pre class="eventlog-details">{{ eventLog.raw.data | json }}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -1410,76 +1555,28 @@
|
||||
</div>
|
||||
|
||||
<div class="card" ng-show="view === 'updates'">
|
||||
<p><label class="control-label">{{ 'app.updates.info.title' | tr }}</label></p>
|
||||
<div class="row">
|
||||
<div class="col-xs-4">
|
||||
<span class="text-muted">{{ 'app.updates.info.description' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-8 text-right">
|
||||
<span ng-show="app.appStoreId">{{ app.manifest.title }} {{ app.upstreamVersion }}</span>
|
||||
<span ng-show="!app.appStoreId">{{ app.manifest.dockerImage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p><label class="control-label">{{ 'app.updatesTabTitle' | tr }}</label></p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'app.updates.info.appId' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ app.id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'app.updates.info.packageVersion' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span ng-show="app.appStoreId"><a ng-href="/#/appstore/{{app.manifest.id}}?version={{app.manifest.version}}">{{ app.manifest.id }}@{{ app.manifest.version }}</a></span>
|
||||
<span ng-show="!app.appStoreId">{{ app.manifest.version }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'app.updates.info.lastUpdated' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ app.updateTime | prettyDate }}</span>
|
||||
<div class="col-md-12">
|
||||
<p>
|
||||
<span ng-bind-html="'app.updates.auto.description' | tr:{ appStoreLink: 'https://www.cloudron.io/store/index.html' }"></span>
|
||||
<span ng-show="app.appStoreId && updates.enableAutomaticUpdate" class="text-success">{{ 'app.updates.auto.enabled' | tr }}</span>
|
||||
<span ng-show="app.appStoreId && !updates.enableAutomaticUpdate" class="text-danger">{{ 'app.updates.auto.disabled' | tr }}</span>
|
||||
<span ng-show="!app.appStoreId" class="text-danger">{{ 'app.updates.info.customAppUpdateInfo' | tr }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row" ng-show="app.appStoreId">
|
||||
<div class="col-md-6" style="line-height: 34px;">
|
||||
<span class="text-success" ng-show="!updates.busyCheck && !(config.update[app.id].manifest.version && config.update[app.id].manifest.version !== app.manifest.version)">{{ 'app.updates.noUpdates' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-md-6 text-right">
|
||||
<button type="button" class="btn" ng-class="config.update[app.id].unstable ? 'btn-danger' : 'btn-success'" ng-click="updates.askUpdate()" ng-disabled="app.error || app.runState === 'stopped'" ng-hide="!(config.update[app.id].manifest.version && config.update[app.id].manifest.version !== app.manifest.version && app.installationState !== 'pending_update') || app.taskId" tooltip-enable="app.error || app.taskId || app.runState === 'stopped'" uib-tooltip="{{ app.error ? 'App is in error state' : 'App is not running' }}">{{ 'app.updateDialog.updateAction' | tr }}</button>
|
||||
<button class="btn btn-default btn-outline" ng-click="updates.check()" ng-disabled="updates.busyCheck"><i class="fas fa-sync-alt fa-spin" ng-show="updates.busyCheck"></i><i class="fas fa-sync-alt" ng-hide="updates.busyCheck"></i> {{ 'settings.updates.checkForUpdatesAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
|
||||
<div class="row" ng-show="!app.appStoreId">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<span class="text-danger pull-right">{{ 'app.updates.info.customAppUpdateInfo' | tr }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="app.appStoreId" class="row">
|
||||
<div class="col-md-12">
|
||||
<label class="control-label">{{ 'app.updates.auto.title' | tr }}</label>
|
||||
<p>{{ 'app.updates.auto.description' | tr }}</p>
|
||||
</div>
|
||||
<div class="col-md-6" style="line-height: 34px;">
|
||||
<span class="text-success" ng-show="app.enableAutomaticUpdate">{{ 'app.updates.auto.enabled' | tr }}</span>
|
||||
<span class="text-danger" ng-hide="app.enableAutomaticUpdate">{{ 'app.updates.auto.disabled' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<button class="btn btn-primary pull-right" uib-tooltip="{{ app.appStoreId ? '' : 'Not available for custom apps' }}" ng-class="{ 'btn-danger': app.enableAutomaticUpdate }" ng-click="updates.toggleAutomaticUpdates()" ng-disabled="updates.busyAutomaticUpdates || !app.appStoreId"><i class="fa fa-circle-notch fa-spin" ng-show="updates.busyAutomaticUpdates"></i> {{ app.enableAutomaticUpdate ? ('app.updates.auto.disableAction' | tr) : ('app.updates.auto.enableAction' | tr) }} </button>
|
||||
<button class="btn pull-right" uib-tooltip="{{ app.appStoreId ? '' : 'Not available for custom apps' }}" ng-class="updates.enableAutomaticUpdate ? 'btn-danger' : 'btn-success'" ng-click="updates.toggleAutomaticUpdates()" ng-disabled="updates.busyAutomaticUpdates || !app.appStoreId"><i class="fa fa-circle-notch fa-spin" ng-show="updates.busyAutomaticUpdates"></i> {{ updates.enableAutomaticUpdate ? ('app.updates.auto.disableAction' | tr) : ('app.updates.auto.enableAction' | tr) }} </button>
|
||||
<!-- check for updates button is always visible -->
|
||||
<button class="btn btn-default btn-outline pull-right" uib-tooltip="{{ app.appStoreId ? '' : 'Not available for custom apps' }}" ng-click="updates.check()" ng-disabled="updates.busyCheck || !app.appStoreId"><i class="fas fa-sync-alt fa-spin" ng-show="updates.busyCheck"></i><i class="fas fa-sync-alt" ng-hide="updates.busyCheck"></i> {{ 'settings.updates.checkForUpdatesAction' | tr }}</button>
|
||||
<!-- show update button only if update available -->
|
||||
<button class="btn pull-right" ng-show="config.update[app.id].manifest.version && config.update[app.id].manifest.version !== app.manifest.version && app.installationState !== 'pending_update' && !app.taskId" ng-class="config.update[app.id].unstable ? 'btn-danger' : 'btn-success'" ng-click="updates.askUpdate()" ng-disabled="app.error || app.runState === 'stopped'" tooltip-enable="app.error || app.taskId || app.runState === 'stopped'" uib-tooltip="{{ app.error ? 'App is in error state' : 'App is not running' }}">{{ 'app.updateDialog.updateAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1506,7 +1603,7 @@
|
||||
<td><i class="fas fa-archive" ng-show="backup.preserveSecs === -1" uib-tooltip="{{ 'backups.listing.tooltipPreservedBackup' | tr }}"></i></td>
|
||||
<!-- <td><div class="hand clipboard" data-clipboard-text="{{ backup.id }}" uib-tooltip="{{ copyBackupIdDone ? ('main.clipboard.copied' | tr) : ('main.clipboard.clickToCopyBackupId' | tr) }}" tooltip-placement="right"><i class="fa fa-copy"></i></div></td> -->
|
||||
<td ng-click="backupDetails.show(backup)" class="hand"><div>v{{ backup.packageVersion }}</div></td>
|
||||
<td ng-click="backupDetails.show(backup)" class="hand"><span uib-tooltip="{{ backup.creationTime | prettyLongDate }}">{{ backup.creationTime | prettyDate }} <b ng-show="backup.label">({{ backup.label }})</b></span></td>
|
||||
<td ng-click="backupDetails.show(backup)" class="hand">{{ backup.creationTime | prettyLongDate }} <b ng-show="backup.label">({{ backup.label }})</b></td>
|
||||
<td class="text-center" style="vertical-align: bottom">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-xs btn-default dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||
|
||||
+195
-73
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular */
|
||||
/* global angular, localStorage, document, FileReader */
|
||||
/* global $ */
|
||||
/* global async */
|
||||
/* global RSTATES */
|
||||
@@ -10,7 +10,8 @@
|
||||
/* global Clipboard */
|
||||
/* global SECRET_PLACEHOLDER */
|
||||
/* global APP_TYPES, STORAGE_PROVIDERS, BACKUP_FORMATS */
|
||||
/* global REGIONS_S3, REGIONS_WASABI, REGIONS_DIGITALOCEAN, REGIONS_EXOSCALE, REGIONS_SCALEWAY, REGIONS_LINODE, REGIONS_OVH, REGIONS_IONOS, REGIONS_UPCLOUD, REGIONS_VULTR, REGIONS_CONTABO */
|
||||
/* global REGIONS_S3, REGIONS_WASABI, REGIONS_DIGITALOCEAN, REGIONS_EXOSCALE, REGIONS_SCALEWAY, REGIONS_LINODE, REGIONS_OVH, REGIONS_IONOS, REGIONS_UPCLOUD, REGIONS_VULTR */
|
||||
/* global onAppClick */
|
||||
|
||||
angular.module('Application').controller('AppController', ['$scope', '$location', '$translate', '$timeout', '$interval', '$route', '$routeParams', 'Client', function ($scope, $location, $translate, $timeout, $interval, $route, $routeParams, Client) {
|
||||
$scope.s3Regions = REGIONS_S3;
|
||||
@@ -76,6 +77,31 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
});
|
||||
};
|
||||
|
||||
$scope.appPostInstallConfirm = {
|
||||
app: {},
|
||||
message: '',
|
||||
confirmed: false,
|
||||
|
||||
show: function (app) {
|
||||
$scope.appPostInstallConfirm.app = app;
|
||||
$scope.appPostInstallConfirm.message = app.manifest.postInstallMessage;
|
||||
$scope.appPostInstallConfirm.confirmed = false;
|
||||
|
||||
$('#appPostInstallConfirmModal').modal('show');
|
||||
|
||||
return false; // prevent propagation and default
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
if (!$scope.appPostInstallConfirm.confirmed) return;
|
||||
|
||||
$scope.appPostInstallConfirm.app.pendingPostInstallConfirmation = false;
|
||||
delete localStorage['confirmPostInstall_' + $scope.appPostInstallConfirm.app.id];
|
||||
|
||||
$('#appPostInstallConfirmModal').modal('hide');
|
||||
}
|
||||
};
|
||||
|
||||
$scope.postInstallMessage = {
|
||||
confirmed: false,
|
||||
openApp: false,
|
||||
@@ -110,6 +136,73 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
}
|
||||
};
|
||||
|
||||
$scope.info = {
|
||||
showDoneChecklist: false,
|
||||
hasOldChecklist: false,
|
||||
|
||||
notes: {
|
||||
busy: true,
|
||||
busySave: false,
|
||||
editing: false,
|
||||
content: '',
|
||||
placeholder: 'Add admin notes here...',
|
||||
|
||||
edit: function () {
|
||||
$scope.info.notes.content = $scope.app.notes === null ? $scope.app.manifest.postInstallMessage : $scope.app.notes;
|
||||
$scope.info.notes.editing = true;
|
||||
|
||||
setTimeout(function () { document.getElementById('adminNotesTextarea').focus(); }, 1);
|
||||
},
|
||||
|
||||
dismiss: function () {
|
||||
$scope.info.notes.content = $scope.app.notes === null ? $scope.app.manifest.postInstallMessage : $scope.app.notes;
|
||||
$scope.info.notes.editing = false;
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.info.notes.busySave = true;
|
||||
|
||||
// skip saving if unchanged from postInstall
|
||||
if ($scope.info.notes.content === $scope.app.manifest.postInstallMessage) {
|
||||
$scope.info.notes.busySave = false;
|
||||
$scope.info.notes.editing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
Client.configureApp($scope.app.id, 'notes', { notes: $scope.info.notes.content }, function (error) {
|
||||
if (error) return console.error('Failed to save notes.', error);
|
||||
|
||||
refreshApp($scope.app.id, function (error) {
|
||||
if (error) return Client.error(error);
|
||||
|
||||
$scope.info.notes.content = $scope.app.notes === null ? $scope.app.manifest.postInstallMessage : $scope.app.notes;
|
||||
$scope.info.notes.busySave = false;
|
||||
$scope.info.notes.editing = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.info.hasOldChecklist = !!Object.keys($scope.app.checklist).find((k) => { return $scope.app.checklist[k].acknowledged; });
|
||||
$scope.info.notes.content = $scope.app.notes === null ? $scope.app.manifest.postInstallMessage : $scope.app.notes;
|
||||
$scope.info.notes.editing = false;
|
||||
$scope.info.notes.busy = false;
|
||||
},
|
||||
|
||||
checklistAck(item, key) {
|
||||
item.acknowledged = true;
|
||||
// item.acknowledged = !item.acknowledged;
|
||||
|
||||
Client.ackAppChecklistItem($scope.app.id, key, item.acknowledged, function (error) {
|
||||
if (error) return console.error('Failed to ack checklist item.', error);
|
||||
$scope.info.hasOldChecklist = true;
|
||||
|
||||
refreshApp($scope.app.id);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.display = {
|
||||
busy: false,
|
||||
error: {},
|
||||
@@ -212,9 +305,9 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
secondaryDomains: {},
|
||||
redirectDomains: [],
|
||||
aliasDomains: [],
|
||||
portBindings: {},
|
||||
portBindingsEnabled: {},
|
||||
portBindingsInfo: {},
|
||||
ports: {},
|
||||
portsEnabled: {},
|
||||
portInfo: {},
|
||||
|
||||
addRedirectDomain: function (event) {
|
||||
event.preventDefault();
|
||||
@@ -276,18 +369,18 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
};
|
||||
});
|
||||
|
||||
$scope.location.portBindingsInfo = angular.extend({}, app.manifest.tcpPorts, app.manifest.udpPorts); // Portbinding map only for information
|
||||
$scope.location.portInfo = angular.extend({}, app.manifest.tcpPorts, app.manifest.udpPorts); // Portbinding map only for information
|
||||
$scope.location.redirectDomains = app.redirectDomains.map(function (a) { return { subdomain: a.subdomain, domain: $scope.domains.filter(function (d) { return d.domain === a.domain; })[0] };});
|
||||
$scope.location.aliasDomains = app.aliasDomains.map(function (a) { return { subdomain: a.subdomain, domain: $scope.domains.filter(function (d) { return d.domain === a.domain; })[0] };});
|
||||
|
||||
// fill the portBinding structures. There might be holes in the app.portBindings, which signalizes a disabled port
|
||||
for (var env in $scope.location.portBindingsInfo) {
|
||||
for (var env in $scope.location.portInfo) {
|
||||
if (app.portBindings && app.portBindings[env]) {
|
||||
$scope.location.portBindings[env] = app.portBindings[env];
|
||||
$scope.location.portBindingsEnabled[env] = true;
|
||||
$scope.location.ports[env] = app.portBindings[env].hostPort;
|
||||
$scope.location.portsEnabled[env] = true;
|
||||
} else {
|
||||
$scope.location.portBindings[env] = $scope.location.portBindingsInfo[env].defaultValue || 0;
|
||||
$scope.location.portBindingsEnabled[env] = false;
|
||||
$scope.location.ports[env] = $scope.location.portInfo[env].defaultValue || 0;
|
||||
$scope.location.portsEnabled[env] = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -307,11 +400,11 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
};
|
||||
}
|
||||
|
||||
// only use enabled ports from portBindings
|
||||
var portBindings = {};
|
||||
for (var env in $scope.location.portBindings) {
|
||||
if ($scope.location.portBindingsEnabled[env]) {
|
||||
portBindings[env] = $scope.location.portBindings[env];
|
||||
// only use enabled ports
|
||||
var ports = {};
|
||||
for (var env in $scope.location.ports) {
|
||||
if ($scope.location.portsEnabled[env]) {
|
||||
ports[env] = $scope.location.ports[env];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,7 +412,7 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
overwriteDns: !!overwriteDns,
|
||||
subdomain: $scope.location.subdomain,
|
||||
domain: $scope.location.domain.domain,
|
||||
portBindings: portBindings,
|
||||
ports: ports,
|
||||
secondaryDomains: secondaryDomains,
|
||||
redirectDomains: $scope.location.redirectDomains.map(function (a) { return { subdomain: a.subdomain, domain: a.domain.domain };}),
|
||||
aliasDomains: $scope.location.aliasDomains.map(function (a) { return { subdomain: a.subdomain, domain: a.domain.domain };})
|
||||
@@ -514,43 +607,49 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
|
||||
busy: false,
|
||||
currentMemoryLimit: 0,
|
||||
memoryLimit: 0,
|
||||
memoryLimit: 0, // RAM
|
||||
memoryTicks: [],
|
||||
|
||||
busyCpuShares: false,
|
||||
currentCpuShares: 0,
|
||||
cpuShares: 0,
|
||||
currentCpuQuota: 0,
|
||||
cpuQuota: 0,
|
||||
|
||||
show: function () {
|
||||
var app = $scope.app;
|
||||
$scope.resources.busy = true;
|
||||
|
||||
$scope.resources.error = {};
|
||||
$scope.resources.currentMemoryLimit = app.memoryLimit || app.manifest.memoryLimit || (256 * 1024 * 1024);
|
||||
$scope.resources.memoryLimit = $scope.resources.currentMemoryLimit;
|
||||
$scope.resources.currentCpuShares = $scope.resources.cpuShares = app.cpuShares;
|
||||
|
||||
Client.getAppLimits(app.id, function (error, limits) {
|
||||
if (error) return console.error(error);
|
||||
Client.memory(function (error, result) {
|
||||
if (error) console.error(error);
|
||||
|
||||
// create ticks starting from manifest memory limit. the memory limit here is currently split into ram+swap (and thus *2 below)
|
||||
// TODO: the *2 will overallocate since 4GB is max swap that cloudron itself allocates
|
||||
// create ticks starting from manifest memory limit. the memory limit here is just RAM
|
||||
$scope.resources.memoryTicks = [];
|
||||
var npow2 = Math.pow(2, Math.ceil(Math.log(limits.memory.memory)/Math.log(2)));
|
||||
for (var i = 256; i <= (npow2*2/1024/1024); i *= 2) {
|
||||
if (i >= (app.manifest.memoryLimit/1024/1024 || 0)) $scope.resources.memoryTicks.push(i * 1024 * 1024);
|
||||
}
|
||||
if (app.manifest.memoryLimit && $scope.resources.memoryTicks[0] !== app.manifest.memoryLimit) {
|
||||
$scope.resources.memoryTicks.unshift(app.manifest.memoryLimit);
|
||||
// we max system memory and current app memory for the case where the user configured the app on another server with more resources
|
||||
var nearest256m = Math.ceil(Math.max(result.memory, $scope.resources.currentMemoryLimit) / (256*1024*1024)) * 256 * 1024 * 1024;
|
||||
var startTick = app.manifest.memoryLimit || (256 * 1024 * 1024);
|
||||
|
||||
for (var i = startTick; i <= nearest256m; i *= 2) {
|
||||
$scope.resources.memoryTicks.push(i);
|
||||
}
|
||||
});
|
||||
|
||||
// for firefox widget update
|
||||
$timeout(function() {
|
||||
$scope.resources.currentCpuQuota = $scope.resources.cpuQuota = app.cpuQuota;
|
||||
$scope.resources.memoryLimit = $scope.resources.currentMemoryLimit;
|
||||
$scope.resources.busy = false;
|
||||
}, 500);
|
||||
},
|
||||
|
||||
submitMemoryLimit: function () {
|
||||
$scope.resources.busy = true;
|
||||
$scope.resources.error = {};
|
||||
|
||||
var memoryLimit = $scope.resources.memoryLimit === $scope.resources.memoryTicks[0] ? 0 : $scope.resources.memoryLimit;
|
||||
Client.configureApp($scope.app.id, 'memory_limit', { memoryLimit: memoryLimit }, function (error) {
|
||||
const tmp = parseInt($scope.resources.memoryLimit);
|
||||
const memoryLimit = tmp === $scope.resources.memoryTicks[0] ? 0 : tmp;
|
||||
|
||||
Client.configureApp($scope.app.id, 'memory_limit', { memoryLimit }, function (error) {
|
||||
if (error && error.statusCode === 400) {
|
||||
$scope.resources.busy = false;
|
||||
$scope.resources.error.memoryLimit = true;
|
||||
@@ -568,19 +667,19 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
});
|
||||
},
|
||||
|
||||
submitCpuShares: function () {
|
||||
$scope.resources.busyCpuShares = true;
|
||||
submitCpuQuota: function () {
|
||||
$scope.resources.busy = true;
|
||||
$scope.resources.error = {};
|
||||
|
||||
Client.configureApp($scope.app.id, 'cpu_shares', { cpuShares: $scope.resources.cpuShares }, function (error) {
|
||||
Client.configureApp($scope.app.id, 'cpu_quota', { cpuQuota: parseInt($scope.resources.cpuQuota) }, function (error) {
|
||||
if (error) return Client.error(error);
|
||||
|
||||
$scope.resources.currentCpuShares = $scope.resources.cpuShares;
|
||||
$scope.resources.currentCpuQuota = $scope.resources.cpuQuota;
|
||||
|
||||
refreshApp($scope.app.id, function (error) {
|
||||
if (error) return Client.error(error);
|
||||
|
||||
$timeout(function () { $scope.resources.busyCpuShares = false; }, 1000);
|
||||
$timeout(function () { $scope.resources.busy = false; }, 1000);
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -688,8 +787,11 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
$scope.storage.error.storageVolumePrefix = error.message;
|
||||
$scope.storage.busyDataDir = false;
|
||||
return;
|
||||
} else if (error) {
|
||||
Client.error(error);
|
||||
$scope.storage.busyDataDir = false;
|
||||
return;
|
||||
}
|
||||
if (error) return Client.error(error);
|
||||
|
||||
$scope.storageDataDirForm.$setPristine();
|
||||
|
||||
@@ -1185,21 +1287,27 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
busyUpdate: false,
|
||||
busyAutomaticUpdates: false,
|
||||
skipBackup: false,
|
||||
enableAutomaticUpdate: true,
|
||||
|
||||
show: function () {
|
||||
$scope.updates.skipBackup = false;
|
||||
$scope.updates.enableAutomaticUpdate = $scope.app.enableAutomaticUpdate;
|
||||
},
|
||||
|
||||
toggleAutomaticUpdates: function () {
|
||||
$scope.updates.busyAutomaticUpdates = true;
|
||||
|
||||
Client.configureApp($scope.app.id, 'automatic_update', { enable: !$scope.app.enableAutomaticUpdate }, function (error) {
|
||||
Client.configureApp($scope.app.id, 'automatic_update', { enable: !$scope.updates.enableAutomaticUpdate }, function (error) {
|
||||
if (error) return Client.error(error);
|
||||
|
||||
refreshApp($scope.app.id, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
$scope.updates.busyAutomaticUpdates = false;
|
||||
$timeout(function () {
|
||||
console.log($scope.updates.enableAutomaticUpdate, $scope.app.enableAutomaticUpdate);
|
||||
$scope.updates.enableAutomaticUpdate = $scope.app.enableAutomaticUpdate;
|
||||
$scope.updates.busyAutomaticUpdates = false;
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -1333,7 +1441,7 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
provider: '',
|
||||
bucket: '',
|
||||
prefix: '',
|
||||
mountPoint: '',
|
||||
mountPoint: '', // for mountpoint
|
||||
accessKeyId: '',
|
||||
secretAccessKey: '',
|
||||
gcsKey: { keyFileName: '', content: '' },
|
||||
@@ -1344,7 +1452,17 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
remotePath: '',
|
||||
password: '',
|
||||
encryptedFilenames: true,
|
||||
mountOptions: {}, // host, port, username, password, remoteDir, diskPath, user, privateKey
|
||||
mountOptions: {
|
||||
host: '',
|
||||
remoteDir: '',
|
||||
username: '',
|
||||
password: '',
|
||||
diskPath: '',
|
||||
user: '',
|
||||
seal: true,
|
||||
port: 22,
|
||||
privateKey: ''
|
||||
},
|
||||
encrypted: false, // helps with ng-required when backupConfig is read from file
|
||||
|
||||
clearForm: function () {
|
||||
@@ -1362,7 +1480,7 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
$scope.importBackup.password = '';
|
||||
$scope.importBackup.encryptedFilenames = true;
|
||||
$scope.importBackup.remotePath = '';
|
||||
$scope.importBackup.mountOptions = {};
|
||||
$scope.importBackup.mountOptions = { host: '', remoteDir: '', username: '', password: '', diskPath: '', seal: true, user: '', port: 22, privateKey: '' };
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
@@ -1382,7 +1500,7 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
// only set provider specific fields, this will clear them in the db
|
||||
if ($scope.s3like(backupConfig.provider)) {
|
||||
backupConfig.bucket = $scope.importBackup.bucket;
|
||||
backupConfig.prefix = '';
|
||||
backupConfig.prefix = $scope.importBackup.prefix;
|
||||
backupConfig.accessKeyId = $scope.importBackup.accessKeyId;
|
||||
backupConfig.secretAccessKey = $scope.importBackup.secretAccessKey;
|
||||
|
||||
@@ -1429,7 +1547,7 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
}
|
||||
} else if (backupConfig.provider === 'gcs') {
|
||||
backupConfig.bucket = $scope.importBackup.bucket;
|
||||
backupConfig.prefix = '';
|
||||
backupConfig.prefix = $scope.importBackup.prefix;
|
||||
try {
|
||||
var serviceAccountKey = JSON.parse($scope.importBackup.gcsKey.content);
|
||||
backupConfig.projectId = serviceAccountKey.project_id;
|
||||
@@ -1449,10 +1567,10 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
}
|
||||
} else if (backupConfig.provider === 'sshfs' || backupConfig.provider === 'cifs' || backupConfig.provider === 'nfs' || backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs') {
|
||||
backupConfig.mountOptions = $scope.importBackup.mountOptions;
|
||||
backupConfig.prefix = '';
|
||||
backupConfig.prefix = $scope.importBackup.prefix;
|
||||
} else if (backupConfig.provider === 'mountpoint') {
|
||||
backupConfig.mountOptions = {};
|
||||
backupConfig.mountPoint = $scope.mountPoint;
|
||||
backupConfig.prefix = $scope.importBackup.prefix;
|
||||
backupConfig.mountPoint = $scope.importBackup.mountPoint;
|
||||
} else if (backupConfig.provider === 'filesystem') {
|
||||
var parts = remotePath.split('/');
|
||||
remotePath = parts.pop() || parts.pop(); // removes any trailing slash. this is basename()
|
||||
@@ -1676,9 +1794,9 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
secondaryDomains: {},
|
||||
needsOverwrite: false,
|
||||
overwriteDns: false,
|
||||
portBindings: {},
|
||||
portBindingsInfo: {},
|
||||
portBindingsEnabled: {},
|
||||
ports: {},
|
||||
portsEnabled: {},
|
||||
portInfo: {},
|
||||
|
||||
show: function (backup) {
|
||||
var app = $scope.app;
|
||||
@@ -1700,11 +1818,11 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
};
|
||||
}
|
||||
|
||||
$scope.clone.portBindingsInfo = angular.extend({}, backup.manifest.tcpPorts, backup.manifest.udpPorts); // Portbinding map only for information
|
||||
$scope.clone.portInfo = angular.extend({}, backup.manifest.tcpPorts, backup.manifest.udpPorts); // Portbinding map only for information
|
||||
// set default ports
|
||||
for (var env in $scope.clone.portBindingsInfo) {
|
||||
$scope.clone.portBindings[env] = $scope.clone.portBindingsInfo[env].defaultValue || 0;
|
||||
$scope.clone.portBindingsEnabled[env] = true;
|
||||
for (var env in $scope.clone.portInfo) {
|
||||
$scope.clone.ports[env] = $scope.clone.portInfo[env].defaultValue || 0;
|
||||
$scope.clone.portsEnabled[env] = true;
|
||||
}
|
||||
|
||||
$('#appCloneModal').modal('show');
|
||||
@@ -1721,11 +1839,11 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
};
|
||||
}
|
||||
|
||||
// only use enabled ports from portBindings
|
||||
var finalPortBindings = {};
|
||||
for (var env in $scope.clone.portBindings) {
|
||||
if ($scope.clone.portBindingsEnabled[env]) {
|
||||
finalPortBindings[env] = $scope.clone.portBindings[env];
|
||||
// only use enabled ports
|
||||
var finalPorts = {};
|
||||
for (var env in $scope.clone.ports) {
|
||||
if ($scope.clone.portsEnabled[env]) {
|
||||
finalPorts[env] = $scope.clone.ports[env];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1733,7 +1851,7 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
subdomain: $scope.clone.subdomain,
|
||||
domain: $scope.clone.domain.domain,
|
||||
secondaryDomains: secondaryDomains,
|
||||
portBindings: finalPortBindings,
|
||||
ports: finalPorts,
|
||||
backupId: $scope.clone.backup.id,
|
||||
overwriteDns: $scope.clone.overwriteDns
|
||||
};
|
||||
@@ -2107,7 +2225,15 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
};
|
||||
|
||||
Object.keys($scope.backupConfig).forEach(function (k) {
|
||||
if ($scope.backupConfig[k] !== SECRET_PLACEHOLDER) tmp[k] = $scope.backupConfig[k];
|
||||
var v = $scope.backupConfig[k];
|
||||
if (v && typeof v === 'object') { // to hide mountOptions.password and the likes
|
||||
tmp[k] = {};
|
||||
Object.keys(v).forEach(function (j) {
|
||||
if (v[j] !== SECRET_PLACEHOLDER) tmp[k][j] = v[j];
|
||||
});
|
||||
} else {
|
||||
if ($scope.backupConfig[k] !== SECRET_PLACEHOLDER) tmp[k] = v;
|
||||
}
|
||||
});
|
||||
|
||||
var filename = 'app-backup-config-' + (new Date()).toISOString().replace(/:|T/g,'-').replace(/\..*/,'') + ' (' + $scope.app.fqdn + ')' + '.json';
|
||||
@@ -2122,16 +2248,12 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
var backupConfig;
|
||||
try {
|
||||
backupConfig = JSON.parse(result.target.result);
|
||||
let prefix = backupConfig.prefix;
|
||||
backupConfig.prefix = ''; // so it can clear the form as well when we apply keys below
|
||||
if (backupConfig.provider === 'filesystem') { // patch the remotePath to have the full path
|
||||
backupConfig.remotePath = (prefix ? prefix + '/' : '') + backupConfig.backupFolder + '/' + backupConfig.remotePath;
|
||||
if (backupConfig.provider === 'filesystem') { // this allows a user to upload a backup to server and import easily with an absolute path
|
||||
backupConfig.remotePath = backupConfig.backupFolder + '/' + backupConfig.remotePath;
|
||||
delete backupConfig.backupFolder;
|
||||
} else {
|
||||
backupConfig.remotePath = (prefix ? prefix + '/' : '') + backupConfig.remotePath;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Unable to parse backup config');
|
||||
console.error('Unable to parse backup config', e);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2157,7 +2279,7 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
|
||||
if ($routeParams.view) { // explicit route in url bar
|
||||
$scope.setView($routeParams.view, true /* skipViewShow */);
|
||||
} else { // default
|
||||
$scope.setView($scope.app.error ? 'repair' : 'display', true /* skipViewShow */);
|
||||
$scope.setView($scope.app.error ? 'repair' : 'info', true /* skipViewShow */);
|
||||
}
|
||||
|
||||
function done() {
|
||||
|
||||
+107
-35
@@ -1,31 +1,33 @@
|
||||
<!-- Modal postinstall confirm -->
|
||||
<div class="modal fade" id="appPostInstallConfirmModal" tabindex="-1" role="dialog">
|
||||
<div class="modal fade" id="appsPostInstallConfirmModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<img ng-src="{{appPostInstallConfirm.app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-info-icon"/>
|
||||
<h5 class="app-info-title">
|
||||
{{ appPostInstallConfirm.app.manifest.title }}
|
||||
<span class="app-info-meta text-small">{{ 'app.appInfo.package' | tr }} <a ng-href="/#/appstore/{{appPostInstallConfirm.app.manifest.id}}?version={{appPostInstallConfirm.app.manifest.version}}">v{{ appPostInstallConfirm.app.manifest.version }}</a> </span>
|
||||
<div class="app-info-title">
|
||||
{{ appPostInstallConfirm.app.manifest.title }}<br/>
|
||||
<span class="text-muted text-small">{{ 'app.appInfo.package' | tr }} <a ng-href="/#/appstore/{{appPostInstallConfirm.app.manifest.id}}?version={{appPostInstallConfirm.app.manifest.version}}">v{{ appPostInstallConfirm.app.manifest.version }}</a> </span>
|
||||
<br/>
|
||||
<span ng-show="appPostInstallConfirm.app.manifest.documentationUrl"><a target="_blank" ng-href="{{appPostInstallConfirm.app.manifest.documentationUrl}}">{{ 'app.docsAction' | tr }}</a> </span>
|
||||
<span ng-show="appPostInstallConfirm.app.manifest.documentationUrl" class="text-small"><a target="_blank" ng-href="{{appPostInstallConfirm.app.manifest.documentationUrl}}">{{ 'app.docsAction' | tr }}</a> </span>
|
||||
<br/>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!--
|
||||
<p ng-show="appPostInstallConfirm.app.manifest.addons.email">{{ 'app.appInfo.ssoEmail' | tr }}</p>
|
||||
<p ng-show="appPostInstallConfirm.app.sso && !appPostInstallConfirm.app.manifest.addons.email">{{ 'app.appInfo.sso' | tr }}</p>
|
||||
|
||||
<div ng-bind-html="appPostInstallConfirm.app.manifest.postInstallMessage | markdown2html"></div>
|
||||
-->
|
||||
<div ng-bind-html="appPostInstallConfirm.message | markdown2html"></div>
|
||||
<div ng-show="appPostInstallConfirm.app.manifest.documentationUrl" ng-bind-html="'app.appInfo.appDocsUrl' | tr:{ docsUrl: appPostInstallConfirm.app.manifest.documentationUrl, title: appPostInstallConfirm.app.manifest.title, forumUrl: (appPostInstallConfirm.app.manifest.forumUrl || 'https://forum.cloudron.io') }"></div>
|
||||
<div ng-repeat="item in appPostInstallConfirm.app.checklist">
|
||||
<div class="checklist-item" ng-hide="item.acknowledged">
|
||||
{{ item.message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="form-group pull-left">
|
||||
<input type="checkbox" id="appPostInstallConfirmCheckbox" ng-model="appPostInstallConfirm.confirmed">
|
||||
<label class="control-label" for="appPostInstallConfirmCheckbox">{{ 'app.appInfo.postInstallConfirmCheckbox' | tr }}</label>
|
||||
</div>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<a class="btn btn-success" ng-href="{{ appPostInstallConfirm.confirmed ? ('https://' + appPostInstallConfirm.app.fqdn) : '' }}" target="_blank" ng-disabled="!appPostInstallConfirm.confirmed" ng-click="appPostInstallConfirm.submit()">{{ 'app.appInfo.openAction' | tr:{ app: appPostInstallConfirm.app.manifest.title } }}</a>
|
||||
<a class="btn btn-success" ng-href="{{ 'https://' + appPostInstallConfirm.app.fqdn }}" target="_blank" ng-click="appPostInstallConfirm.submit()">{{ 'app.appInfo.openAction' | tr:{ app: appPostInstallConfirm.app.manifest.title } }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,7 +59,7 @@
|
||||
</div>
|
||||
<div id="previewIcon" class="app-custom-icon" ng-click="applinksEdit.showCustomIconSelector()">
|
||||
<img ng-src="{{ applinksEdit.iconUrl() || 'img/appicon_fallback.png' }}" fallback-icon="img/appicon_fallback.png" onerror="imageErrorHandler(this)"/>
|
||||
<div class="overlay"></div>
|
||||
<i class="picture-edit-indicator fa fa-pencil-alt"></i>
|
||||
</div>
|
||||
<a href="" style="font-weight: normal;" ng-click="applinksEdit.resetCustomIcon()">{{ 'app.applinks.clearIconAction' | tr }}</a> - <span class="text-small">{{ 'app.applinks.clearIconDescription' | tr }}</span>
|
||||
<input type="file" id="applinksEditIconFileInput" style="display: none" accept="image/png"/>
|
||||
@@ -130,34 +132,36 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="view-header" ng-show="installedApps.length > 0">
|
||||
<h1 class="view-header" ng-show="installedApps.length > 0" style="padding-right: 0;">
|
||||
{{ 'apps.title' | tr }}
|
||||
<div class="view-header-search-bar">
|
||||
<form class="form-inline">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" style="width: 300px" placeholder="{{ 'apps.searchPlaceholder' | tr }}" id="appSearch" ng-model="appSearch"/>
|
||||
<input type="text" class="form-control" style="width: 300px" placeholder="{{ 'apps.searchPlaceholder' | tr }} ( / )" id="appSearch" ng-model="appSearch"/>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-class="{ 'active': showFilter, 'btn-warning': showFilter || selectedTags.length || selectedState.state || !selectedGroup._unset || !selectedDomain._alldomains }" ng-click="showFilter = !showFilter"><i class="fas fa-filter"></i></button>
|
||||
<button class="btn btn-default" type="button" ng-class="{ 'active': showFilter }" ng-click="toggleFilter()"><i class="fas fa-filter"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div ng-show="showFilter" class="view-header-filter-bar">
|
||||
<form class="form-inline">
|
||||
<multiselect ng-model="selectedGroup" ng-show="user.isAtLeastAdmin && groups.length > 1" ms-header="{{ selectedGroup.name }}" options="group.name for group in groups" data-multiple="false" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<multiselect ng-model="selectedState" ng-show="user.isAtLeastAdmin" ms-header="{{ 'apps.stateFilterHeader' | tr }}" ms-selected="{{ selectedState }}" options="state.label for state in states" data-multiple="false"></multiselect>
|
||||
<multiselect ng-model="selectedTags" ng-show="user.isAtLeastAdmin && tags.length > 0" ms-header="{{ 'apps.tagsFilterHeaderAll' | tr }}" ms-selected="{{ 'apps.tagsFilterHeader' | tr:{ tags: selectedTags.join(', ') } }}" options="tag for tag in tags" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<multiselect ng-model="selectedDomain" data-compare-by="domain" ms-selected="{{ selectedDomain.domain }}" options="domain.domain for domain in filterDomains" data-multiple="false" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<button class="btn btn-warning" ng-disabled="!selectedTags.length && !selectedState.state && selectedGroup._unset && selectedDomain._alldomains" ng-click="clearAllFilter()">{{ 'apps.filter.clearAll' | tr }}</button>
|
||||
<button class="btn btn-default" type="button" ng-click="toggleView()"><i class="fas" ng-class="{ 'fa-list': view === VIEWS.GRID, 'fa-grip': view === VIEWS.LIST }"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<div ng-show="showFilter" class="view-header-filter-bar">
|
||||
<form class="form-inline">
|
||||
<multiselect ng-model="selectedGroup" ng-show="user.isAtLeastAdmin && groups.length > 1" ms-header="{{ selectedGroup.name }}" options="group.name for group in groups" data-multiple="false" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<multiselect ng-model="selectedState" ng-show="user.isAtLeastAdmin" ms-header="{{ 'apps.stateFilterHeader' | tr }}" ms-selected="{{ selectedState }}" options="state.label for state in states" data-multiple="false"></multiselect>
|
||||
<multiselect ng-model="selectedTags" ng-show="user.isAtLeastAdmin && tags.length > 0" ms-header="{{ 'apps.tagsFilterHeaderAll' | tr }}" ms-selected="{{ 'apps.tagsFilterHeader' | tr:{ tags: selectedTags.join(', ') } }}" options="tag for tag in tags" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<multiselect ng-model="selectedDomain" data-compare-by="domain" ms-selected="{{ selectedDomain.domain }}" options="domain.domain for domain in filterDomains" data-multiple="false" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<!-- <button class="btn btn-primary" ng-disabled="!selectedTags.length && !selectedState.state && selectedGroup._unset && selectedDomain._alldomains" ng-click="clearAllFilter()">{{ 'apps.filter.clearAll' | tr }}</button> -->
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length > 0">
|
||||
<div class="app-grid">
|
||||
<div class="grid-item" ng-class="{ 'stopped': app.runState === 'stopped' }" ng-repeat="app in installedApps | selectedGroupAccessFilter:selectedGroup | selectedStateFilter:selectedState | selectedTagFilter:selectedTags | selectedDomainFilter:selectedDomain | appSearchFilter:appSearch | orderBy:'fqdn'">
|
||||
<div class="grid-item-content" uib-tooltip="{{ app.fqdn }}">
|
||||
<a ng-show="app.type !== APP_TYPES.LINK && isOperator(app)" ng-href="#/app/{{ app.id}}/display" class="btn btn-lg btn-default grid-item-action"><i class="fas fa-cog"></i></a>
|
||||
<div class="app-grid" ng-show="view === VIEWS.GRID">
|
||||
<div class="grid-item" ng-class="{ 'stopped': app.runState === 'stopped' }" ng-repeat="app in installedApps | selectedGroupAccessFilter:selectedGroup | selectedStateFilter:selectedState | selectedTagFilter:selectedTags | selectedDomainFilter:selectedDomain | appSearchFilter:appSearch | orderBy:orderByFilter">
|
||||
<div class="grid-item-content" uib-tooltip="{{ app.fqdn }}" tooltip-append-to-body="true">
|
||||
<a ng-show="app.type !== APP_TYPES.LINK && isOperator(app)" ng-href="#/app/{{ app.id}}/info" class="btn btn-lg btn-default grid-item-action"><i class="fas fa-cog"></i></a>
|
||||
<div ng-show="app.type === APP_TYPES.LINK && isOperator(app)" ng-click="applinksEdit.show(app)" class="btn btn-lg btn-default grid-item-action"><i class="fas fa-cog"></i></div>
|
||||
<a ng-href="{{ app | applicationLink }}" ng-click="onAppClick(app, $event)" target="_blank">
|
||||
<div class="grid-item-top">
|
||||
@@ -190,12 +194,80 @@
|
||||
</div>
|
||||
|
||||
</a>
|
||||
|
||||
<!-- we check the version here because the box updater does not know when an app gets updated -->
|
||||
<div class="app-update-badge" ng-click="showAppConfigure(app, 'updates')" ng-show="config.update[app.id].manifest.version && config.update[app.id].manifest.version !== app.manifest.version && (app | installSuccess) && !(app.error || app.runState === 'stopped')">
|
||||
<i class="fa fa-arrow-up fa-inverse"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- we check the version here because the box updater does not know when an app gets updated -->
|
||||
<div class="app-update-badge" ng-click="showAppConfigure(app, 'updates')" ng-show="config.update[app.id].manifest.version && config.update[app.id].manifest.version !== app.manifest.version && (app | installSuccess) && !(app.error || app.runState === 'stopped')" uib-tooltip="Update Available">
|
||||
<i class="fa fa-arrow-up fa-inverse"></i>
|
||||
</div>
|
||||
|
||||
<div class="app-checklist-badge" ng-click="showAppConfigure(app, 'info')" ng-show="pendingChecklistItems(app)">
|
||||
{{ pendingChecklistItems(app) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-list card card-large" ng-show="view === VIEWS.LIST">
|
||||
<table class="table table-hover" style="margin: 0;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 32px" class="hand" ng-click="setOrderBy('status')"><i ng-show="orderBy === 'status'" class="fas fa-arrow-{{ orderByReverse ? 'up' : 'down' }}-long"></i></th>
|
||||
<th style="width: 32px"> </th>
|
||||
<th style="width: 35%" class="hand" ng-click="setOrderBy('location')">{{ 'app.display.label' | tr }} <i ng-show="orderBy === 'location'" class="fas fa-arrow-{{ orderByReverse ? 'up' : 'down' }}-long"></i></th>
|
||||
<th style="width: 30%" class="hand hide-mobile" ng-click="setOrderBy('app')">App Title<i ng-show="orderBy === 'app'" class="fas fa-arrow-{{ orderByReverse ? 'up' : 'down' }}-long"></i></th>
|
||||
<th style="width: 32px"> </th>
|
||||
<th style="width: 32px" class="hand hide-mobile text-center" ng-click="setOrderBy('sso')"><i class="fas fa-user-lock"></i> <i ng-show="orderBy === 'sso'" class="fas fa-arrow-{{ orderByReverse ? 'up' : 'down' }}-long"></i></th>
|
||||
<th style="width:160px" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="app-list-item" ng-repeat="app in installedApps | selectedGroupAccessFilter:selectedGroup | selectedStateFilter:selectedState | selectedTagFilter:selectedTags | selectedDomainFilter:selectedDomain | appSearchFilter:appSearch | orderBy:orderByFilter:orderByReverse" uib-tooltip="{{ app | appProgressMessage }}">
|
||||
<td class="elide-table-cell">
|
||||
<i class="fa fa-circle" ng-class="app | installationStateClass" uib-tooltip="{{ app | installationStateLabel }}"></i>
|
||||
</td>
|
||||
<td class="elide-table-cell app-list-app-link-cell">
|
||||
<a ng-href="{{ app | applicationLink }}" ng-click="onAppClick(app, $event)" target="_blank" class="app-list-app-link">
|
||||
<img ng-src="{{ app.iconUrl || 'img/appicon_fallback.png' }}" fallback-icon="img/appicon_fallback.png" onerror="imageErrorHandler(this)" class="app-list-item-icon"/>
|
||||
</a>
|
||||
</td>
|
||||
<td class="elide-table-cell app-list-app-link-cell">
|
||||
<a ng-href="{{ app | applicationLink }}" ng-click="onAppClick(app, $event)" target="_blank" class="app-list-app-link">
|
||||
<span style="font-size: 16px;">{{ app.label || app.subdomain || app.fqdn }}</span><br/>
|
||||
<span class="text-muted text-small">{{ app.fqdn.indexOf('http') === 0 ? app.fqdn : 'https://'+app.fqdn }}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td class="elide-table-cell hide-mobile">{{ app.manifest.title || 'App Link' }}</td>
|
||||
<td class="elide-table-cell hide-mobile text-center">
|
||||
<a class="badge badge-danger" ng-show="pendingChecklistItems(app)" ng-href="#/app/{{ app.id}}/info">{{ pendingChecklistItems(app) }}</a>
|
||||
</td>
|
||||
<td class="elide-table-cell hide-mobile text-center">
|
||||
<div ng-show="app.type !== APP_TYPES.LINK">
|
||||
<i class="fa-brands fa-openid" ng-show="app.ssoAuth && app.manifest.addons.oidc" uib-tooltip="{{ 'apps.auth.openid' | tr }}"></i>
|
||||
<i class="fas fa-user" ng-show="app.ssoAuth && (!app.manifest.addons.oidc && !app.manifest.addons.email)" uib-tooltip="{{ 'apps.auth.sso' | tr }}"></i>
|
||||
<i class="far fa-user" ng-show="!app.ssoAuth && !app.manifest.addons.email" uib-tooltip="{{ 'apps.auth.nosso' | tr }}"></i>
|
||||
<i class="fas fa-envelope" ng-show="app.manifest.addons.email" uib-tooltip="{{ 'apps.auth.email' | tr }}"></i>
|
||||
</div>
|
||||
</td>
|
||||
<td class="elide-table-cell text-right">
|
||||
<span ng-show="isOperator(app)">
|
||||
<a class="btn btn-xs btn-success" style="padding: 1px 7px;" ng-show="config.update[app.id].manifest.version && config.update[app.id].manifest.version !== app.manifest.version && (app | installSuccess) && !(app.error || app.runState === 'stopped')" ng-href="#/app/{{ app.id}}/updates" uib-tooltip="Update Available"><i class="fa fa-arrow-up"></i></a>
|
||||
|
||||
<div class="btn-group btn-group-xs" role="group">
|
||||
<a class="btn btn-xs btn-default" ng-show="app.type !== APP_TYPES.LINK" ng-href="{{ '/frontend/logs.html?appId=' + app.id }}" target="_blank" tooltip-append-to-body="true" uib-tooltip="{{ 'app.logsActionTooltip' | tr }}"><i class="fas fa-align-left"></i></a>
|
||||
<a class="btn btn-xs btn-default" ng-show="app.type !== APP_TYPES.PROXIED && app.type !== APP_TYPES.LINK" ng-href="{{ '/frontend/terminal.html?id=' + app.id }}" target="_blank" tooltip-append-to-body="true" uib-tooltip="{{ 'app.terminalActionTooltip' | tr }}"><i class="fa fa-terminal"></i></a>
|
||||
<a class="btn btn-xs btn-default" ng-show="app.manifest.addons.localstorage" ng-href="{{ '/frontend/filemanager.html#/home/app/' + app.id }}" target="_blank" tooltip-append-to-body="true" uib-tooltip="{{ 'app.filemanagerActionTooltip' | tr }}"><i class="fas fa-folder"></i></a>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-xs btn-default" ng-show="app.type === APP_TYPES.LINK" ng-click="applinksEdit.show(app)" uib-tooltip="Configure Applink"><i class="fa fa-cog"></i></button>
|
||||
<a class="btn btn-xs btn-default" ng-show="app.type !== APP_TYPES.LINK" ng-href="#/app/{{ app.id}}/info" uib-tooltip="Configure App"><i class="fa fa-cog"></i></a>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br/>
|
||||
<div>
|
||||
{{ 'apps.apps.count' | tr:{ count: (installedApps | selectedGroupAccessFilter:selectedGroup | selectedStateFilter:selectedState | selectedTagFilter:selectedTags | selectedDomainFilter:selectedDomain | appSearchFilter:appSearch).length } }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+85
-14
@@ -4,6 +4,7 @@
|
||||
/* global $:false */
|
||||
/* global APP_TYPES */
|
||||
/* global onAppClick */
|
||||
/* global localStorage, document, FileReader */
|
||||
|
||||
angular.module('Application').controller('AppsController', ['$scope', '$translate', '$interval', '$location', 'Client', function ($scope, $translate, $interval, $location, Client) {
|
||||
var ALL_DOMAINS_DOMAIN = { _alldomains: true, domain: 'All Domains' }; // dummy record for the single select filter
|
||||
@@ -32,6 +33,15 @@ angular.module('Application').controller('AppsController', ['$scope', '$translat
|
||||
$scope.showFilter = false;
|
||||
$scope.filterActive = false;
|
||||
|
||||
$scope.VIEWS = {
|
||||
GRID: 'grid',
|
||||
LIST: 'list'
|
||||
};
|
||||
$scope.view = $scope.VIEWS.GRID;
|
||||
|
||||
$scope.orderBy = 'location'; // or app, status, sso
|
||||
$scope.orderByReverse = false;
|
||||
|
||||
$scope.allUsers = [];
|
||||
$scope.allGroups = [];
|
||||
|
||||
@@ -45,6 +55,59 @@ angular.module('Application').controller('AppsController', ['$scope', '$translat
|
||||
if (tr['app.states.updateAvailable']) $scope.states[3].label = tr['app.states.updateAvailable'];
|
||||
});
|
||||
|
||||
$scope.pendingChecklistItems = function (app) {
|
||||
if (!app.checklist) return 0;
|
||||
return Object.keys(app.checklist).filter(function (key) { return !app.checklist[key].acknowledged; }).length;
|
||||
};
|
||||
|
||||
$scope.setOrderBy = function (by) {
|
||||
if (by === $scope.orderBy) {
|
||||
$scope.orderByReverse = !$scope.orderByReverse;
|
||||
} else {
|
||||
$scope.orderBy = by;
|
||||
$scope.orderByReverse = false;
|
||||
}
|
||||
|
||||
localStorage.appsOrderBy = by;
|
||||
if ($scope.orderByReverse) localStorage.appsOrderByReverse = true;
|
||||
else localStorage.removeItem('appsOrderByReverse');
|
||||
};
|
||||
|
||||
// for sorting/grouping
|
||||
$scope.orderByFilter = function (item) {
|
||||
if ($scope.orderBy === 'app') return item.manifest.title || 'App Link';
|
||||
if ($scope.orderBy === 'status') return item.installationState + '-' + item.runState;
|
||||
if ($scope.orderBy === 'sso') return item.sso;
|
||||
return item.label || item.fqdn;
|
||||
};
|
||||
|
||||
$scope.setView = function (view) {
|
||||
if (view !== $scope.VIEWS.LIST && view !== $scope.VIEWS.GRID) return;
|
||||
|
||||
$scope.view = view;
|
||||
localStorage.appsView = view;
|
||||
};
|
||||
|
||||
$scope.toggleView = function () {
|
||||
$scope.view = $scope.view === $scope.VIEWS.GRID ? $scope.VIEWS.LIST : $scope.VIEWS.GRID;
|
||||
localStorage.appsView = $scope.view;
|
||||
};
|
||||
|
||||
$scope.toggleFilter = function () {
|
||||
$scope.showFilter = !$scope.showFilter;
|
||||
|
||||
if ($scope.showFilter) localStorage.appsShowFilter = true;
|
||||
else localStorage.removeItem('appsShowFilter');
|
||||
|
||||
// clear on hide
|
||||
if (!$scope.showFilter) {
|
||||
$scope.selectedState = $scope.states[0];
|
||||
$scope.selectedTags = [];
|
||||
$scope.selectedGroup = GROUP_ACCESS_UNSET;
|
||||
$scope.selectedDomain = ALL_DOMAINS_DOMAIN;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('selectedTags', function (newVal, oldVal) {
|
||||
if (newVal === oldVal) return;
|
||||
|
||||
@@ -74,35 +137,24 @@ angular.module('Application').controller('AppsController', ['$scope', '$translat
|
||||
|
||||
$scope.onAppClick = function (app, $event) { onAppClick(app, $event, $scope.isOperator(app), $scope); };
|
||||
|
||||
$scope.clearAllFilter = function () {
|
||||
$scope.selectedState = $scope.states[0];
|
||||
$scope.selectedTags = [];
|
||||
$scope.selectedGroup = GROUP_ACCESS_UNSET;
|
||||
$scope.selectedDomain = ALL_DOMAINS_DOMAIN;
|
||||
};
|
||||
|
||||
$scope.appPostInstallConfirm = {
|
||||
app: {},
|
||||
message: '',
|
||||
confirmed: false,
|
||||
|
||||
show: function (app) {
|
||||
$scope.appPostInstallConfirm.app = app;
|
||||
$scope.appPostInstallConfirm.message = app.manifest.postInstallMessage;
|
||||
$scope.appPostInstallConfirm.confirmed = false;
|
||||
|
||||
$('#appPostInstallConfirmModal').modal('show');
|
||||
$('#appsPostInstallConfirmModal').modal('show');
|
||||
|
||||
return false; // prevent propagation and default
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
if (!$scope.appPostInstallConfirm.confirmed) return;
|
||||
|
||||
$scope.appPostInstallConfirm.app.pendingPostInstallConfirmation = false;
|
||||
delete localStorage['confirmPostInstall_' + $scope.appPostInstallConfirm.app.id];
|
||||
|
||||
$('#appPostInstallConfirmModal').modal('hide');
|
||||
$('#appsPostInstallConfirmModal').modal('hide');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -251,6 +303,12 @@ angular.module('Application').controller('AppsController', ['$scope', '$translat
|
||||
});
|
||||
});
|
||||
|
||||
$scope.setView(localStorage.appsView);
|
||||
|
||||
$scope.orderBy = localStorage.appsOrderBy || 'location';
|
||||
$scope.orderByReverse = !!localStorage.appsOrderByReverse;
|
||||
$scope.showFilter = !!localStorage.appsShowFilter;
|
||||
|
||||
if (!$scope.user.isAtLeastAdmin) return;
|
||||
|
||||
// load local settings and apply tag filter
|
||||
@@ -300,7 +358,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$translat
|
||||
// setup all the dialog focus handling
|
||||
['applinksAddModal', 'applinksEditModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
$(this).find('autofocus]:first').focus();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -311,4 +369,17 @@ angular.module('Application').controller('AppsController', ['$scope', '$translat
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
|
||||
function keyboardHandler(event) {
|
||||
if (event.key === '/') {
|
||||
document.getElementById('appSearch').focus();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', keyboardHandler);
|
||||
|
||||
$scope.$on('$destroy', function () {
|
||||
document.removeEventListener('keydown', keyboardHandler);
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -69,17 +69,17 @@
|
||||
</div>
|
||||
|
||||
<div class="has-error text-center" ng-show="appInstall.error.port">{{ appInstall.error.port }}</div>
|
||||
<div ng-repeat="(env, info) in appInstall.portBindingsInfo">
|
||||
<div ng-repeat="(env, info) in appInstall.portInfo">
|
||||
<ng-form name="portInfo_form">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!appInstallForm.itemName{{$index}}.$dirty && appInstall.error.port) || (portInfo_form.itemName{{$index}}.$dirty && portInfo_form.itemName{{$index}}.$invalid) }">
|
||||
<label class="control-label" for="inputPortInfo{{env}}"><input type="checkbox" ng-model="appInstall.portBindingsEnabled[env]">
|
||||
<label class="control-label" for="inputPortInfo{{env}}"><input type="checkbox" ng-model="appInstall.portsEnabled[env]">
|
||||
{{ info.title }}
|
||||
<sup>
|
||||
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})"><i class="fa fa-question-circle"></i></a>
|
||||
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}}. {{info.portCount >=1 ? (info.portCount + ' ports. ') : ''}} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})"><i class="fa fa-question-circle"></i></a>
|
||||
</sup>
|
||||
<small style="padding-left: 5px;" ng-show="info.readOnly">{{ 'appstore.installDialog.portReadOnly' | tr }}</small>
|
||||
</label>
|
||||
<input type="number" class="form-control" ng-model="appInstall.portBindings[env]" ng-disabled="!appInstall.portBindingsEnabled[env]" ng-readonly="info.readOnly" id="inputPortInfo{{env}}" later-name="itemName{{$index}}" min="{{hostPortMin}}" max="{{hostPortMax}}" required>
|
||||
<input type="number" class="form-control" ng-model="appInstall.ports[env]" ng-disabled="!appInstall.portsEnabled[env]" ng-readonly="info.readOnly" id="inputPortInfo{{env}}" later-name="itemName{{$index}}" min="{{hostPortMin}}" max="{{hostPortMax}}" required>
|
||||
<p class="text-small text-warning text-bold" ng-show="appInstall.domain.provider === 'cloudflare'">{{ 'appstore.installDialog.cloudflarePortWarning' | tr }} </p>
|
||||
</div>
|
||||
</ng-form>
|
||||
@@ -281,8 +281,9 @@
|
||||
<!-- appstore login -->
|
||||
<div ng-show="ready && !validSubscription" class="container card card-small appstore-login ng-cloak">
|
||||
<div class="col-md-12 text-center">
|
||||
<h1 ng-show="appstoreLogin.register">{{ 'appstore.accountDialog.titleSignUp' | tr }}</h1>
|
||||
<h1 ng-hide="appstoreLogin.register">{{ 'appstore.accountDialog.titleLogin' | tr }}</h1>
|
||||
<h1 ng-show="appstoreLogin.setupType === 'signup'">{{ 'appstore.accountDialog.titleSignUp' | tr }}</h1>
|
||||
<h1 ng-show="appstoreLogin.setupType === 'login'">{{ 'appstore.accountDialog.titleLogin' | tr }}</h1>
|
||||
<h1 ng-show="appstoreLogin.setupType === 'setupToken'">{{ 'appstore.accountDialog.titleToken' | tr }}</h1>
|
||||
</div>
|
||||
<div class="col-md-12 text-center">
|
||||
<p>{{ 'appstore.accountDialog.description' | tr }}</p>
|
||||
@@ -293,54 +294,121 @@
|
||||
<div class="col-md-12">
|
||||
<br/>
|
||||
|
||||
<form name="appstoreLoginForm" role="form" novalidate ng-submit="appstoreLogin.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<div ng-show="appstoreLogin.setupType === 'signup'">
|
||||
<form name="appstoreSignupForm" role="form" novalidate ng-submit="appstoreLogin.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (appstoreLoginForm.email.$dirty && appstoreLoginForm.email.$invalid) || appstoreLogin.error.generic }">
|
||||
<div class="form-group" ng-class="{ 'has-error': (appstoreSignupForm.email.$dirty && appstoreSignupForm.email.$invalid) || appstoreLogin.error.generic }">
|
||||
<label class="control-label">{{ 'appstore.accountDialog.email' | tr }}</label>
|
||||
<input type="email" class="form-control" ng-model="appstoreLogin.email" id="inputAppstoreLoginEmail" name="email" required autofocus>
|
||||
<div class="control-label" ng-show="(!appstoreLoginForm.email.$dirty && appstoreLogin.error.email) || (appstoreLoginForm.email.$dirty && appstoreLoginForm.email.$invalid) || appstoreLogin.error.email">
|
||||
<small class="text-danger" ng-show="appstoreLogin.error.email">{{ appstoreLogin.error.email }}</small>
|
||||
<input type="email" class="form-control" ng-model="appstoreLogin.email" id="inputAppstoreSignupEmail" name="email" required autofocus>
|
||||
<div class="control-label" ng-show="(!appstoreSignupForm.email.$dirty && appstoreLogin.error.email) || (appstoreSignupForm.email.$dirty && appstoreSignupForm.email.$invalid) || appstoreLogin.error.email">
|
||||
<small class="text-danger" ng-show="appstoreLogin.error.email">{{ appstoreLogin.error.email }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (!appstoreLoginForm.password.$dirty && appstoreLogin.error.password) || (appstoreLoginForm.password.$dirty && appstoreLoginForm.password.$invalid) || appstoreLogin.error.generic }">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!appstoreSignupForm.password.$dirty && appstoreLogin.error.signupPassword) || (appstoreSignupForm.password.$dirty && appstoreSignupForm.password.$invalid) || appstoreLogin.error.generic }">
|
||||
<label class="control-label">{{ 'appstore.accountDialog.password' | tr }}</label>
|
||||
<input type="password" class="form-control" ng-model="appstoreLogin.password" id="inputAppstoreSignupPassword" name="password" required password-reveal>
|
||||
<div class="control-label" ng-show="(!appstoreSignupForm.password.$dirty && appstoreLogin.error.signupPassword) || (appstoreSignupForm.password.$dirty && appstoreSignupForm.password.$invalid)">
|
||||
<small ng-show="!appstoreSignupForm.password.$dirty && appstoreLogin.error.signupPassword">{{ 'appstore.accountDialog.errorWrongPassword' | tr }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="appstoreLogin.termsAccepted" ng-required="true"><span ng-bind-html="'appstore.accountDialog.licenseCheckbox' | tr:{ licenseLink: 'https://cloudron.io/legal/license.html' }"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<center>
|
||||
<button type="submit" class="btn btn-lg btn-success" ng-disabled="appstoreSignupForm.$invalid || appstoreLogin.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="appstoreLogin.busy"></i> {{ 'appstore.accountDialog.createAccountAction' | tr }}
|
||||
</button>
|
||||
</center>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div ng-show="appstoreLogin.setupType === 'login'">
|
||||
<form name="appstoreLoginForm" role="form" novalidate ng-submit="appstoreLogin.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (appstoreLoginForm.email.$dirty && appstoreLoginForm.email.$invalid) || appstoreLogin.error.generic }">
|
||||
<label class="control-label">{{ 'appstore.accountDialog.email' | tr }}</label>
|
||||
<input type="email" class="form-control" ng-model="appstoreLogin.email" name="email" required autofocus>
|
||||
<div class="control-label" ng-show="(!appstoreLoginForm.email.$dirty && appstoreLogin.error.email) || (appstoreLoginForm.email.$dirty && appstoreLoginForm.email.$invalid) || appstoreLogin.error.email">
|
||||
<small class="text-danger" ng-show="appstoreLogin.error.email">{{ appstoreLogin.error.email }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (!appstoreLoginForm.password.$dirty && appstoreLogin.error.loginPassword) || (appstoreLoginForm.password.$dirty && appstoreLoginForm.password.$invalid) || appstoreLogin.error.generic }">
|
||||
<label class="control-label">{{ 'appstore.accountDialog.password' | tr }}</label>
|
||||
<input type="password" class="form-control" ng-model="appstoreLogin.password" id="inputAppstoreLoginPassword" name="password" required password-reveal>
|
||||
<div class="control-label" ng-show="(!appstoreLoginForm.password.$dirty && appstoreLogin.error.password) || (appstoreLoginForm.password.$dirty && appstoreLoginForm.password.$invalid)">
|
||||
<small ng-show="!appstoreLoginForm.password.$dirty && appstoreLogin.error.password">{{ 'appstore.accountDialog.errorWrongPassword' | tr }}</small>
|
||||
<div class="control-label" ng-show="(!appstoreLoginForm.password.$dirty && appstoreLogin.error.loginPassword) || (appstoreLoginForm.password.$dirty && appstoreLoginForm.password.$invalid)">
|
||||
<small ng-show="!appstoreLoginForm.password.$dirty && appstoreLogin.error.loginPassword">{{ 'appstore.accountDialog.errorWrongPassword' | tr }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-hide="appstoreLogin.register" ng-class="{ 'has-error': appstoreLogin.error.totpToken }">
|
||||
<div class="form-group" ng-class="{ 'has-error': appstoreLogin.error.totpToken }">
|
||||
<label class="control-label">{{ 'appstore.accountDialog.2faToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="appstoreLogin.totpToken" id="inputAppstoreLoginTotpToken" name="totpToken">
|
||||
<div class="control-label" ng-show="appstoreLogin.error.totpToken">
|
||||
<small ng-show="appstoreLogin.error.totpToken">{{ appstoreLogin.error.totpToken }}</small>
|
||||
<small ng-show="appstoreLogin.error.totpToken">{{ appstoreLogin.error.totpToken }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="appstoreLogin.termsAccepted"><span ng-bind-html="'appstore.accountDialog.licenseCheckbox' | tr:{ licenseLink: 'https://cloudron.io/legal/license.html' }"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<center>
|
||||
<button type="submit" class="btn btn-lg btn-success" ng-disabled="appstoreLoginForm.$invalid || appstoreLogin.busy || !appstoreLogin.termsAccepted">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="appstoreLogin.busy"></i> <span ng-hide="appstoreLogin.register">{{ 'appstore.accountDialog.loginAction' | tr }}</span><span ng-show="appstoreLogin.register">{{ 'appstore.accountDialog.createAccountAction' | tr }}</span>
|
||||
</button>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="appstoreLogin.termsAccepted" ng-required="true"><span ng-bind-html="'appstore.accountDialog.licenseCheckbox' | tr:{ licenseLink: 'https://cloudron.io/legal/license.html' }"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<center>
|
||||
<button type="submit" class="btn btn-lg btn-success" ng-disabled="appstoreLoginForm.$invalid || appstoreLogin.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="appstoreLogin.busy"></i> {{ 'appstore.accountDialog.loginAction' | tr }}
|
||||
</button>
|
||||
</center>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div ng-show="appstoreLogin.setupType === 'setupToken'">
|
||||
<form name="appstoreSetupTokenForm" role="form" novalidate ng-submit="appstoreLogin.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': appstoreLogin.error.setupToken }">
|
||||
<label class="control-label">{{ 'appstore.accountDialog.setupToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="appstoreLogin.setupToken" id="inputAppstoreSetupToken" name="setupToken" ng-required="true">
|
||||
<div class="control-label" ng-show="appstoreLogin.error.setupToken">
|
||||
<small ng-show="appstoreLogin.error.setupToken">{{ appstoreLogin.error.setupToken }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="appstoreLogin.termsAccepted" ng-required="true"><span ng-bind-html="'appstore.accountDialog.licenseCheckbox' | tr:{ licenseLink: 'https://cloudron.io/legal/license.html' }"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<a href="" ng-click="appstoreLogin.register = true" ng-hide="appstoreLogin.register">{{ 'appstore.accountDialog.switchToSignUpAction' | tr }}</a>
|
||||
<a href="" ng-click="appstoreLogin.register = false" ng-show="appstoreLogin.register">{{ 'appstore.accountDialog.switchToLoginAction' | tr }}</a>
|
||||
</center>
|
||||
<center>
|
||||
<button type="submit" class="btn btn-lg btn-success" ng-disabled="appstoreSetupTokenForm.$invalid || appstoreLogin.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="appstoreLogin.busy"></i> {{ 'appstore.accountDialog.setupWithTokenAction' | tr }}
|
||||
</button>
|
||||
</center>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<br/>
|
||||
|
||||
<center>
|
||||
<a href="" ng-click="appstoreLogin.setupType = 'signup'" ng-show="appstoreLogin.setupType === 'login'">{{ 'appstore.accountDialog.switchToSignUpAction' | tr }}</a>
|
||||
<a href="" ng-click="appstoreLogin.setupType = 'login'" ng-show="appstoreLogin.setupType === 'signup' || appstoreLogin.setupType === 'setupToken'">{{ 'appstore.accountDialog.switchToLoginAction' | tr }}</a>
|
||||
<span ng-show="appstoreLogin.setupType !== 'setupToken'"> or <a href="" ng-click="appstoreLogin.setupType = 'setupToken'">use a setup token</a></span>
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular:false */
|
||||
/* global angular:false, document, window, localStorage, FileReader */
|
||||
/* global $:false */
|
||||
/* global async */
|
||||
/* global ERROR */
|
||||
@@ -64,6 +64,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
{ id: 'notes', icon: 'fa fa-sticky-note', label: 'Notes'},
|
||||
{ id: 'project', icon: 'fas fa-project-diagram', label: 'Project Management'},
|
||||
{ id: 'sync', icon: 'fa fa-sync-alt', label: 'File Sync'},
|
||||
{ id: 'voip', icon: 'fa fa-headset', label: 'VoIP'},
|
||||
{ id: 'vpn', icon: 'fa fa-user-secret', label: 'VPN'},
|
||||
{ id: 'wiki', icon: 'fab fa-wikipedia-w', label: 'Wiki'},
|
||||
];
|
||||
@@ -121,7 +122,8 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
subdomain: '',
|
||||
domain: null, // object and not the string
|
||||
secondaryDomains: {},
|
||||
portBindings: {},
|
||||
ports: {},
|
||||
portsEnabled: {},
|
||||
mediaLinks: [],
|
||||
certificateFile: null,
|
||||
certificateFileName: '',
|
||||
@@ -146,7 +148,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
$scope.appInstall.subdomain = '';
|
||||
$scope.appInstall.domain = null;
|
||||
$scope.appInstall.secondaryDomains = {};
|
||||
$scope.appInstall.portBindings = {};
|
||||
$scope.appInstall.ports = {};
|
||||
$scope.appInstall.state = 'appInfo';
|
||||
$scope.appInstall.mediaLinks = [];
|
||||
$scope.appInstall.certificateFile = null;
|
||||
@@ -175,13 +177,13 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
var app = $scope.appInstall.app;
|
||||
var DEFAULT_MEMORY_LIMIT = 1024 * 1024 * 256;
|
||||
|
||||
var needed = app.manifest.memoryLimit || DEFAULT_MEMORY_LIMIT; // RAM+Swap
|
||||
var needed = app.manifest.memoryLimit || DEFAULT_MEMORY_LIMIT; // RAM
|
||||
var used = Client.getInstalledApps().reduce(function (prev, cur) {
|
||||
if (cur.runState === RSTATES.STOPPED) return prev;
|
||||
|
||||
return prev + (cur.memoryLimit || cur.manifest.memoryLimit || DEFAULT_MEMORY_LIMIT);
|
||||
}, 0);
|
||||
var totalMemory = ($scope.memory.memory + $scope.memory.swap) * 1.5;
|
||||
var totalMemory = $scope.memory.memory * 2;
|
||||
var available = (totalMemory || 0) - used;
|
||||
|
||||
var enoughResourcesAvailable = (available - needed) >= 0;
|
||||
@@ -217,9 +219,9 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
};
|
||||
}
|
||||
|
||||
$scope.appInstall.portBindingsInfo = angular.extend({}, $scope.appInstall.app.manifest.tcpPorts, $scope.appInstall.app.manifest.udpPorts); // Portbinding map only for information
|
||||
$scope.appInstall.portBindings = {}; // This is the actual model holding the env:port pair
|
||||
$scope.appInstall.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
|
||||
$scope.appInstall.portInfo = angular.extend({}, $scope.appInstall.app.manifest.tcpPorts, $scope.appInstall.app.manifest.udpPorts); // Portbinding map only for information
|
||||
$scope.appInstall.ports = {}; // This holds the env:port pair
|
||||
$scope.appInstall.portsEnabled = {}; // This holds the enabled/disabled flag
|
||||
|
||||
var manifest = app.manifest;
|
||||
$scope.appInstall.optionalSso = !!manifest.optionalSso;
|
||||
@@ -231,8 +233,8 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
// set default ports
|
||||
var allPorts = angular.extend({}, $scope.appInstall.app.manifest.tcpPorts, $scope.appInstall.app.manifest.udpPorts);
|
||||
for (var env in allPorts) {
|
||||
$scope.appInstall.portBindings[env] = allPorts[env].defaultValue || 0;
|
||||
$scope.appInstall.portBindingsEnabled[env] = true;
|
||||
$scope.appInstall.ports[env] = allPorts[env].defaultValue || 0;
|
||||
$scope.appInstall.portsEnabled[env] = true;
|
||||
}
|
||||
|
||||
$('#appInstallModal').modal('show');
|
||||
@@ -252,11 +254,11 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
};
|
||||
}
|
||||
|
||||
// only use enabled ports from portBindings
|
||||
var finalPortBindings = {};
|
||||
for (var env in $scope.appInstall.portBindings) {
|
||||
if ($scope.appInstall.portBindingsEnabled[env]) {
|
||||
finalPortBindings[env] = $scope.appInstall.portBindings[env];
|
||||
// only use enabled ports from ports
|
||||
var finalPorts = {};
|
||||
for (var env in $scope.appInstall.ports) {
|
||||
if ($scope.appInstall.portsEnabled[env]) {
|
||||
finalPorts[env] = $scope.appInstall.ports[env];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,7 +274,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
subdomain: $scope.appInstall.subdomain || '',
|
||||
domain: $scope.appInstall.domain.domain,
|
||||
secondaryDomains: secondaryDomains,
|
||||
portBindings: finalPortBindings,
|
||||
ports: finalPorts,
|
||||
accessRestriction: finalAccessRestriction,
|
||||
cert: $scope.appInstall.certificateFile,
|
||||
key: $scope.appInstall.keyFile,
|
||||
@@ -415,22 +417,24 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
email: '',
|
||||
password: '',
|
||||
totpToken: '',
|
||||
register: true,
|
||||
setupType: 'login',
|
||||
termsAccepted: false,
|
||||
setupToken: '',
|
||||
|
||||
submit: function () {
|
||||
$scope.appstoreLogin.error = {};
|
||||
$scope.appstoreLogin.busy = true;
|
||||
|
||||
Client.registerCloudron($scope.appstoreLogin.email, $scope.appstoreLogin.password, $scope.appstoreLogin.totpToken, $scope.appstoreLogin.register, function (error) {
|
||||
var func = $scope.appstoreLogin.setupToken ? Client.registerCloudronWithSetupToken.bind(null, $scope.appstoreLogin.setupToken) : Client.registerCloudron.bind(null, $scope.appstoreLogin.email, $scope.appstoreLogin.password, $scope.appstoreLogin.totpToken, $scope.appstoreLogin.setupType === 'register');
|
||||
func(function (error) {
|
||||
if (error) {
|
||||
$scope.appstoreLogin.busy = false;
|
||||
|
||||
if (error.statusCode === 409) {
|
||||
$scope.appstoreLogin.error.email = 'An account with this email already exists';
|
||||
$scope.appstoreLogin.password = '';
|
||||
$scope.appstoreLoginForm.email.$setPristine();
|
||||
$scope.appstoreLoginForm.password.$setPristine();
|
||||
$scope.appstoreSignupForm.email.$setPristine();
|
||||
$scope.appstoreSignupForm.password.$setPristine();
|
||||
$('#inputAppstoreLoginEmail').focus();
|
||||
} else if (error.statusCode === 412) {
|
||||
if (error.message.indexOf('TOTP token missing') !== -1) {
|
||||
@@ -441,7 +445,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
$scope.appstoreLogin.totpToken = '';
|
||||
setTimeout(function () { $('#inputAppstoreLoginTotpToken').focus(); }, 0);
|
||||
} else {
|
||||
$scope.appstoreLogin.error.password = 'Wrong email or password';
|
||||
$scope.appstoreLogin.error.loginPassword = 'Wrong email or password';
|
||||
$scope.appstoreLogin.password = '';
|
||||
$('#inputAppstoreLoginPassword').focus();
|
||||
$scope.appstoreLoginForm.password.$setPristine();
|
||||
@@ -453,11 +457,18 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
$scope.appstoreLogin.password = '';
|
||||
$scope.appstoreLoginForm.email.$setPristine();
|
||||
$scope.appstoreLoginForm.password.$setPristine();
|
||||
$scope.appstoreSignupForm.email.$setPristine();
|
||||
$scope.appstoreSignupForm.password.$setPristine();
|
||||
$('#inputAppstoreLoginEmail').focus();
|
||||
} else {
|
||||
console.error(error);
|
||||
$scope.appstoreLogin.error.generic = error.message;
|
||||
}
|
||||
} else if (error.statusCode === 402) {
|
||||
$scope.appstoreLogin.error.setupToken = 'Invalid or expired setup token';
|
||||
$scope.appstoreLogin.setupToken = '';
|
||||
$scope.appstoreSetupTokenForm.setupToken.$setPristine();
|
||||
$('#inputAppstoreSetupToken').focus();
|
||||
} else {
|
||||
console.error(error);
|
||||
$scope.appstoreLogin.error.generic = error.message || 'Please retry later';
|
||||
@@ -777,10 +788,12 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
getSubscription(function (error, validSubscription) {
|
||||
if (error) console.error('Failed to get subscription.', error);
|
||||
|
||||
// autofocus login
|
||||
if (!validSubscription) setTimeout(function () { $('[name=appstoreLoginForm]').find('[autofocus]:first').focus(); }, 1000);
|
||||
|
||||
$scope.validSubscription = validSubscription;
|
||||
$scope.ready = true;
|
||||
|
||||
|
||||
// refresh everything in background
|
||||
Client.getAppstoreApps(function (error) { if (error) console.error('Failed to fetch apps.', error); });
|
||||
Client.refreshConfig(); // refresh domain, user, group limit etc
|
||||
@@ -827,10 +840,5 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
|
||||
});
|
||||
});
|
||||
|
||||
// autofocus if appstore login is shown
|
||||
$scope.$watch('validSubscription', function (newValue/*, oldValue */) {
|
||||
if (!newValue) setTimeout(function () { $('[name=appstoreLoginForm]').find('[autofocus]:first').focus(); }, 1000);
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
|
||||
@@ -32,8 +32,10 @@
|
||||
</div>
|
||||
<br/>
|
||||
<p class="text-muted">{{ 'backups.backupDetails.list' | tr:{ appCount: backupDetails.backup.contents.length } }}:</p>
|
||||
<span ng-repeat="app in backupDetails.backup.contents | orderBy:['label','fqdn']">
|
||||
<a ng-href="/#/app/{{app.id}}/backups">{{ app.label || app.fqdn }}</a><span ng-hide="$last">,</span>
|
||||
<span ng-repeat="content in backupDetails.backup.contents | orderBy:['label','fqdn']">
|
||||
<a ng-if="content.fqdn" ng-href="/#/app/{{content.id}}/backups">{{ content.label || content.fqdn }}</a>
|
||||
<a ng-if="!content.fqdn" ng-href="/#/eventlog?search={{content.id}}">{{ content.id }}</a>
|
||||
<span ng-hide="$last">,</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -177,7 +179,7 @@
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.mountPoint || (configureBackupForm.mountPoint.$dirty && !configureBackup.mountPoint) }" ng-show="configureBackup.provider === 'mountpoint'">
|
||||
<label class="control-label" for="inputConfigureMountPoint">{{ 'backups.configureBackupStorage.mountPoint' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountPoint" id="inputConfigureMountPoint" name="mountPoint" ng-disabled="configureBackup.busy" placeholder="/mnt/backups" ng-required="configureBackup.provider === 'mountpoint'">
|
||||
<p ng-show="configureBackup.provider === 'mointpoint'" ng-bind-html="'backups.configureBackupStorage.mountPointDescription' | tr:{ providerDocsLink: 'https://docs.cloudron.io/backups/#'+configureBackup.provider }"></p>
|
||||
<p ng-show="configureBackup.provider === 'mountpoint'" ng-bind-html="'backups.configureBackupStorage.mountPointDescription' | tr:{ providerDocsLink: 'https://docs.cloudron.io/backups/#'+configureBackup.provider }"></p>
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
@@ -388,48 +390,46 @@
|
||||
</div>
|
||||
|
||||
<a href="" ng-click="configureBackup.advancedVisible = true" ng-hide="configureBackup.advancedVisible">{{ 'backups.configureBackupStorage.advancedSettings' | tr }}</a>
|
||||
<div uib-collapse="!configureBackup.advancedVisible">
|
||||
<div uib-collapse="!configureBackup.advancedVisible">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'backups.configureBackupStorage.memoryLimit' | tr }}: <b>{{ configureBackup.memoryLimit | prettyBinarySize:'800 MB' }}</b></label>
|
||||
<label class="control-label" for="sliderConfigureBackupMemoryLimit">{{ 'backups.configureBackupStorage.memoryLimit' | tr }}: <b>{{ configureBackup.memoryLimit | prettyBinarySize:'1024 MB' }}</b></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.memoryLimitDescription' | tr }}</p>
|
||||
<div style="padding: 0 10px;">
|
||||
<slider id="sliderConfigureBackupMemoryLimit" ng-model="configureBackup.memoryLimit" step="134217728" tooltip="hide" ticks="configureBackup.memoryTicks" ticks-snap-bounds="67108864"></slider>
|
||||
</div>
|
||||
<input type="range" id="sliderConfigureBackupMemoryLimit" ng-model="configureBackup.memoryLimit" step="{{ 256*1024*1024 }}" min="{{ MIN_MEMORY_LIMIT }}" max="{{ MAX_MEMORY_LIMIT }}" />
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="s3like(configureBackup.provider)">
|
||||
<label class="control-label">{{ 'backups.configureBackupStorage.uploadPartSize' | tr }}: <b>{{ configureBackup.uploadPartSize | prettyBinarySize:'Default (50 MiB)' }}</b></label>
|
||||
<label class="control-label" for="sliderConfigureBackupUploadPartSize">{{ 'backups.configureBackupStorage.uploadPartSize' | tr }}: <b>{{ configureBackup.uploadPartSize | prettyBinarySize:'Default (50 MiB)' }}</b></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.uploadPartSizeDescription' | tr }}</p>
|
||||
<div style="padding: 0 10px;">
|
||||
<slider id="sliderConfigureBackupUploadPartSize" ng-model="configureBackup.uploadPartSize" step="1048576" tooltip="hide" ticks="configureBackup.uploadPartSizeTicks" ticks-snap-bounds="2097152"></slider>
|
||||
</div>
|
||||
<input type="range" id="sliderConfigureBackupUploadPartSize" ng-model="configureBackup.uploadPartSize" list="uploadPartSizeTicks" step="{{ 1024*1024 }}" min="{{ 1024*1024 }}" max="{{ 1024*1024*1024 }}" />
|
||||
<datalist id="uploadPartSizeTicks">
|
||||
<option value="{{ 1024*1024 }}"></option>
|
||||
<option value="{{ 64*1024*1024 }}"></option>
|
||||
<option value="{{ 128*1024*1024 }}"></option>
|
||||
<option value="{{ 256*1024*1024 }}"></option>
|
||||
<option value="{{ 512*1024*1024 }}"></option>
|
||||
<option value="{{ 1024*1024*1024 }}"></option>
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="configureBackup.format === 'rsync' && configureBackup.provider !== 'noop'">
|
||||
<label class="control-label">{{ 'backups.configureBackupStorage.uploadConcurrency' | tr }}: <b>{{ configureBackup.syncConcurrency }}</b></label>
|
||||
<label class="control-label" for="sliderConfigureBackupSyncConcurrency">{{ 'backups.configureBackupStorage.uploadConcurrency' | tr }}: <b>{{ configureBackup.syncConcurrency }}</b></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.uploadConcurrencyDescription' | tr }}</p>
|
||||
<div style="padding: 0 10px;">
|
||||
<slider id="sliderConfigureBackupSyncConcurrency" ng-model="configureBackup.syncConcurrency" tooltip="hide" min="10" max="200" step="10"></slider>
|
||||
</div>
|
||||
<input type="range" id="sliderConfigureBackupSyncConcurrency" ng-model="configureBackup.syncConcurrency" step="10" min="10" max="200" />
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="configureBackup.format === 'rsync' && (s3like(configureBackup.provider) || configureBackup.provider === 'gcs')">
|
||||
<label class="control-label">{{ 'backups.configureBackupStorage.downloadConcurrency' | tr }}: <b>{{ configureBackup.downloadConcurrency }}</b></label>
|
||||
<label class="control-label" for="sliderConfigureBackupDownloadConcurrency">{{ 'backups.configureBackupStorage.downloadConcurrency' | tr }}: <b>{{ configureBackup.downloadConcurrency }}</b></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.downloadConcurrencyDescription' | tr }}</p>
|
||||
<div style="padding: 0 10px;">
|
||||
<slider id="sliderConfigureBackupCopyConcurrency" ng-model="configureBackup.downloadConcurrency" tooltip="hide" min="10" max="200" step="10"></slider>
|
||||
</div>
|
||||
<input type="range" id="sliderConfigureBackupDownloadConcurrency" ng-model="configureBackup.downloadConcurrency" step="10" min="10" max="200" />
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="configureBackup.format === 'rsync' && (s3like(configureBackup.provider) || configureBackup.provider === 'gcs')">
|
||||
<label class="control-label">{{ 'backups.configureBackupStorage.copyConcurrency' | tr }}: <b>{{ configureBackup.copyConcurrency }}</b></label>
|
||||
<label class="control-label" for="sliderConfigureBackupCopyConcurrency">{{ 'backups.configureBackupStorage.copyConcurrency' | tr }}: <b>{{ configureBackup.copyConcurrency }}</b></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.copyConcurrencyDescription' | tr }}
|
||||
<span ng-show="configureBackup.provider === 'digitalocean-spaces'">{{ 'backups.configureBackupStorage.copyConcurrencyDigitalOceanNote' | tr }}</span>
|
||||
</p>
|
||||
<div style="padding: 0 10px;">
|
||||
<slider id="sliderConfigureBackupCopyConcurrency" ng-model="configureBackup.copyConcurrency" tooltip="hide" min="10" max="500" step="10"></slider>
|
||||
</div>
|
||||
<input type="range" id="sliderConfigureBackupCopyConcurrency" ng-model="configureBackup.copyConcurrency" step="10" min="10" max="500" />
|
||||
</div>
|
||||
|
||||
</div> <!-- advanced -->
|
||||
@@ -605,7 +605,7 @@
|
||||
<tr ng-repeat="backup in backups">
|
||||
<td><i class="fas fa-archive" ng-show="backup.preserveSecs === -1" uib-tooltip="{{ 'backups.listing.tooltipPreservedBackup' | tr }}"></i></td>
|
||||
<td ng-click="backupDetails.show(backup)" class="hand">v{{ backup.packageVersion }}</td>
|
||||
<td ng-click="backupDetails.show(backup)" class="hand"><span uib-tooltip="{{ backup.creationTime | prettyLongDate }}">{{ backup.creationTime | prettyDate }} <b ng-show="backup.label">({{ backup.label }})</b></span></td>
|
||||
<td ng-click="backupDetails.show(backup)" class="hand">{{ backup.creationTime | prettyLongDate }} <b ng-show="backup.label">({{ backup.label }})</b></td>
|
||||
<td ng-click="backupDetails.show(backup)" class="hand">
|
||||
<span ng-show="!backup.contents.length">{{ 'backups.listing.noApps' | tr }}</span>
|
||||
<span ng-show="backup.contents.length">{{ 'backups.listing.appCount' | tr:{ appCount: backup.contents.length } }}</span>
|
||||
|
||||
@@ -7,7 +7,8 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.SECRET_PLACEHOLDER = SECRET_PLACEHOLDER;
|
||||
$scope.MIN_MEMORY_LIMIT = 800 * 1024 * 1024;
|
||||
$scope.MIN_MEMORY_LIMIT = 1024 * 1024 * 1024; // 1 GB
|
||||
$scope.MAX_MEMORY_LIMIT = $scope.MIN_MEMORY_LIMIT; // set later
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
@@ -463,7 +464,6 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
|
||||
memoryTicks: [],
|
||||
memoryLimit: $scope.MIN_MEMORY_LIMIT,
|
||||
uploadPartSizeTicks: [],
|
||||
uploadPartSize: 50 * 1024 * 1024,
|
||||
copyConcurrency: '',
|
||||
downloadConcurrency: '',
|
||||
@@ -477,7 +477,7 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
username: '',
|
||||
password: '',
|
||||
diskPath: '',
|
||||
seal: false,
|
||||
seal: true,
|
||||
user: '',
|
||||
port: 22,
|
||||
privateKey: ''
|
||||
@@ -506,7 +506,7 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.configureBackup.copyConcurrency = $scope.configureBackup.provider === 's3' ? 500 : 10;
|
||||
|
||||
$scope.configureBackup.disk = null;
|
||||
$scope.configureBackup.mountOptions = { host: '', remoteDir: '', username: '', password: '', diskPath: '', seal: false, user: '', port: 22, privateKey: '' };
|
||||
$scope.configureBackup.mountOptions = { host: '', remoteDir: '', username: '', password: '', diskPath: '', seal: true, user: '', port: 22, privateKey: '' };
|
||||
},
|
||||
|
||||
show: function () {
|
||||
@@ -540,25 +540,14 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.configureBackup.useHardlinks = !$scope.backupConfig.noHardlinks;
|
||||
$scope.configureBackup.chown = $scope.backupConfig.chown;
|
||||
|
||||
var limits = $scope.backupConfig.limits || {};
|
||||
$scope.configureBackup.memoryLimit = limits.memoryLimit;
|
||||
const limits = $scope.backupConfig.limits || {};
|
||||
|
||||
$scope.configureBackup.memoryLimit = limits.memoryLimit ? Math.max(limits.memoryLimit, $scope.MIN_MEMORY_LIMIT) : $scope.MIN_MEMORY_LIMIT;
|
||||
$scope.configureBackup.uploadPartSize = limits.uploadPartSize || ($scope.configureBackup.provider === 'scaleway-objectstorage' ? 100 * 1024 * 1024 : 10 * 1024 * 1024);
|
||||
$scope.configureBackup.downloadConcurrency = limits.downloadConcurrency || ($scope.backupConfig.provider === 's3' ? 30 : 10);
|
||||
$scope.configureBackup.syncConcurrency = limits.syncConcurrency || ($scope.backupConfig.provider === 's3' ? 20 : 10);
|
||||
$scope.configureBackup.copyConcurrency = limits.copyConcurrency || ($scope.backupConfig.provider === 's3' ? 500 : 10);
|
||||
|
||||
var totalMemory = Math.max(($scope.memory.memory + $scope.memory.swap) * 1.5, 2 * 1024 * 1024);
|
||||
$scope.configureBackup.memoryTicks = [ $scope.MIN_MEMORY_LIMIT ];
|
||||
for (var i = 1024; i <= totalMemory/1024/1024; i *= 2) {
|
||||
$scope.configureBackup.memoryTicks.push(i * 1024 * 1024);
|
||||
}
|
||||
|
||||
$scope.configureBackup.uploadPartSizeTicks = [ 5 * 1024 * 1024 ];
|
||||
for (var j = 32; j <= 1 * 1024; j *= 2) { // 5 GB is max for s3. but let's keep things practical for now. we upload 3 parts in parallel
|
||||
$scope.configureBackup.uploadPartSizeTicks.push(j * 1024 * 1024);
|
||||
}
|
||||
|
||||
var mountOptions = $scope.backupConfig.mountOptions || {};
|
||||
$scope.configureBackup.mountOptions = {
|
||||
host: mountOptions.host || '',
|
||||
@@ -607,7 +596,7 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
schedulePattern: $scope.backupConfig.schedulePattern,
|
||||
retentionPolicy: $scope.backupConfig.retentionPolicy,
|
||||
limits: {
|
||||
memoryLimit: $scope.configureBackup.memoryLimit,
|
||||
memoryLimit: parseInt($scope.configureBackup.memoryLimit),
|
||||
},
|
||||
};
|
||||
if ($scope.configureBackup.password) {
|
||||
@@ -663,6 +652,8 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
} else if (backupConfig.provider === 'digitalocean-spaces') {
|
||||
backupConfig.region = 'us-east-1';
|
||||
}
|
||||
|
||||
backupConfig.limits.uploadPartSize = parseInt($scope.configureBackup.uploadPartSize);
|
||||
} else if (backupConfig.provider === 'gcs') {
|
||||
backupConfig.bucket = $scope.configureBackup.bucket;
|
||||
backupConfig.prefix = $scope.configureBackup.prefix;
|
||||
@@ -713,12 +704,10 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
backupConfig.noHardlinks = !$scope.configureBackup.useHardlinks;
|
||||
}
|
||||
|
||||
backupConfig.limits.uploadPartSize = $scope.configureBackup.uploadPartSize;
|
||||
|
||||
if (backupConfig.format === 'rsync') {
|
||||
backupConfig.limits.downloadConcurrency = $scope.configureBackup.downloadConcurrency;
|
||||
backupConfig.limits.syncConcurrency = $scope.configureBackup.syncConcurrency;
|
||||
backupConfig.limits.copyConcurrency = $scope.configureBackup.copyConcurrency;
|
||||
backupConfig.limits.downloadConcurrency = parseInt($scope.configureBackup.downloadConcurrency);
|
||||
backupConfig.limits.syncConcurrency = parseInt($scope.configureBackup.syncConcurrency);
|
||||
backupConfig.limits.copyConcurrency = parseInt($scope.configureBackup.copyConcurrency);
|
||||
}
|
||||
|
||||
Client.setBackupConfig(backupConfig, function (error) {
|
||||
@@ -766,7 +755,7 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.configureBackup.error.backupFolder = true;
|
||||
}
|
||||
} else {
|
||||
console.error('Unable to change provider.', error);
|
||||
$scope.configureBackup.error.generic = error.message;
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -794,14 +783,23 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
});
|
||||
|
||||
$scope.backups.forEach(function (backup) {
|
||||
backup.contents = [];
|
||||
backup.contents = []; // { id, label, fqdn }
|
||||
backup.dependsOn.forEach(function (appBackupId) {
|
||||
let match = appBackupId.match(/app_(.*?)_.*/); // *? means non-greedy
|
||||
if (!match) return;
|
||||
if (match[1].indexOf('.') !== -1) { // newer backups have fqdn in them
|
||||
if (appsByFqdn[match[1]]) backup.contents.push(appsByFqdn[match[1]]);
|
||||
if (!match) return; // for example, 'mail'
|
||||
const app = appsById[match[1]];
|
||||
if (app) {
|
||||
backup.contents.push({
|
||||
id: app.id,
|
||||
label: app.label,
|
||||
fqdn: app.fqdn
|
||||
});
|
||||
} else {
|
||||
if (appsById[match[1]]) backup.contents.push(appsById[match[1]]);
|
||||
backup.contents.push({
|
||||
id: match[1],
|
||||
label: null,
|
||||
fqdn: null
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -850,6 +848,8 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
if (error) console.error(error);
|
||||
|
||||
$scope.memory = memory;
|
||||
var nearestGb = Math.ceil($scope.memory.memory / (1024*1024*1024)) * 1024 * 1024 * 1024;
|
||||
$scope.MAX_MEMORY_LIMIT = nearestGb;
|
||||
|
||||
fetchBackups();
|
||||
getBackupConfig();
|
||||
@@ -887,7 +887,7 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
// setup all the dialog focus handling
|
||||
['configureBackupModal', 'editBackupModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
$(this).find('[autofocus]:first').focus();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
<!-- Modal change avatar -->
|
||||
<div class="modal fade" id="avatarChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'branding.changeLogo.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body branding-avatar-selector">
|
||||
<img id="previewAvatar" width="128" height="128" ng-src="{{ avatarChange.avatarUrl() }}"/>
|
||||
<input type="file" id="avatarFileInput" style="display: none" accept="image/*"/>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div class="grid">
|
||||
<div class="item" ng-repeat="avatar in avatarChange.availableAvatars" style="background-image: url('{{avatar.data || avatar.url}}');" ng-click="avatarChange.setPreviewAvatar(avatar)"></div>
|
||||
<div class="item add" ng-click="avatarChange.showCustomAvatarSelector()"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="avatarChange.setAvatar()"> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'branding.changeLogo.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body branding-avatar-selector">
|
||||
<img id="previewAvatar" width="128" height="128" ng-src="{{ avatarChange.avatarUrl() }}"/>
|
||||
<input type="file" id="avatarFileInput" style="display: none" accept="image/*"/>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div class="grid">
|
||||
<div class="item" ng-repeat="avatar in avatarChange.availableAvatars" style="background-image: url('{{avatar.data || avatar.url}}');" ng-click="avatarChange.setPreviewAvatar(avatar)"></div>
|
||||
<div class="item add" ng-click="avatarChange.showCustomAvatarSelector()"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="avatarChange.setAvatar()"> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -49,7 +49,19 @@
|
||||
</div>
|
||||
<div class="branding-avatar" ng-click="avatarChange.showChangeAvatar()">
|
||||
<img ng-src="{{ about.avatarUrl() }}"/>
|
||||
<div class="overlay"></div>
|
||||
<i class="picture-edit-indicator fa fa-pencil-alt"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div>
|
||||
<label class="control-label">{{ 'branding.backgroundImage' | tr }}</label>
|
||||
<div class="branding-background" ng-click="background.selectNew()">
|
||||
<img ng-src="{{ background.url() }}" onerror="this.src = '/img/background-image-placeholder.svg'"/>
|
||||
<i class="picture-edit-indicator fa fa-pencil-alt"></i>
|
||||
</div>
|
||||
<a href="" ng-show="!background.cleared" ng-click="background.clear()">{{ 'branding.clearBackgroundImage' | tr }}</a>
|
||||
<input type="file" id="backgroundFileInput" style="display: none" accept="image/*"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +73,7 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="about.submit()" ng-disabled="(!about.avatar && !aboutForm.$dirty) || aboutForm.$invalid || about.busy"><i class="fa fa-circle-notch fa-spin" ng-show="about.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="about.submit()" ng-disabled="false && (!about.avatar && !aboutForm.$dirty) || aboutForm.$invalid || about.busy"><i class="fa fa-circle-notch fa-spin" ng-show="about.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
/* global $:false */
|
||||
|
||||
angular.module('Application').controller('BrandingController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (Client.getUserInfo().role !== 'owner') $location.path('/'); });
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.config = Client.getConfig();
|
||||
@@ -134,6 +134,76 @@ angular.module('Application').controller('BrandingController', ['$scope', '$loca
|
||||
fr.readAsDataURL(event.target.files[0]);
|
||||
};
|
||||
|
||||
$scope.background = {
|
||||
enabled: false,
|
||||
file: null,
|
||||
src: null,
|
||||
cleared: false,
|
||||
newImageFile: null,
|
||||
cacheBusting: Date.now(),
|
||||
|
||||
url() {
|
||||
if ($scope.background.cleared) return '/img/background-image-placeholder.svg';
|
||||
else if ($scope.background.src) return $scope.background.src;
|
||||
else return `${Client.apiOrigin}/api/v1/cloudron/background?${$scope.background.cacheBusting}`;
|
||||
},
|
||||
|
||||
selectNew() {
|
||||
document.getElementById('backgroundFileInput').click();
|
||||
},
|
||||
|
||||
submit(callback) {
|
||||
if ($scope.background.cleared) {
|
||||
Client.changeCloudronBackground(null, callback);
|
||||
} else if ($scope.background.newImageFile) {
|
||||
Client.changeCloudronBackground($scope.background.newImageFile, callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
|
||||
clear() {
|
||||
$scope.background.cleared = true;
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('backgroundFileInput').onchange = function (event) {
|
||||
const fr = new FileReader();
|
||||
fr.onload = function () {
|
||||
const image = new Image();
|
||||
image.onload = function () {
|
||||
// convert and scale to webp max 4k
|
||||
const maxWidth = 4096;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
|
||||
if (image.naturalWidth > maxWidth) {
|
||||
canvas.width = maxWidth;
|
||||
canvas.height = (image.naturalHeight / image.naturalWidth) * maxWidth;
|
||||
} else {
|
||||
canvas.width = image.naturalWidth;
|
||||
canvas.height = image.naturalHeight;
|
||||
}
|
||||
|
||||
canvas.getContext('2d').drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||
canvas.toBlob((blob) => {
|
||||
$scope.$apply(function () {
|
||||
const myImage = new File([blob], 'background.webp', { type: blob.type });
|
||||
|
||||
$scope.background.cleared = false;
|
||||
$scope.background.newImageFile = myImage;
|
||||
$scope.background.src = URL.createObjectURL(myImage);
|
||||
});
|
||||
}, 'image/webp');
|
||||
|
||||
$scope.background.file = event.target.files[0];
|
||||
};
|
||||
|
||||
image.src = fr.result;
|
||||
};
|
||||
fr.readAsDataURL(event.target.files[0]);
|
||||
};
|
||||
|
||||
$scope.about = {
|
||||
busy: false,
|
||||
error: {},
|
||||
@@ -141,7 +211,7 @@ angular.module('Application').controller('BrandingController', ['$scope', '$loca
|
||||
avatar: null,
|
||||
avatarBlob: null,
|
||||
|
||||
avatarUrl: function () {
|
||||
avatarUrl() {
|
||||
if ($scope.about.avatar) {
|
||||
return $scope.about.avatar.data || $scope.about.avatar.url;
|
||||
} else {
|
||||
@@ -149,12 +219,22 @@ angular.module('Application').controller('BrandingController', ['$scope', '$loca
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function () {
|
||||
refresh() {
|
||||
$scope.about.cloudronName = $scope.config.cloudronName;
|
||||
$scope.about.avatar = null;
|
||||
|
||||
Client.hasCloudronBackground(function (error, result) {
|
||||
if (error) return console.error('Failed to get background state.', error);
|
||||
|
||||
$scope.background.enabled = result;
|
||||
$scope.background.file = null;
|
||||
$scope.background.src = null;
|
||||
$scope.background.newImageFile = null;
|
||||
$scope.background.cacheBusting = Date.now();
|
||||
});
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
submit() {
|
||||
$scope.about.error.name = null;
|
||||
$scope.about.busy = true;
|
||||
|
||||
@@ -182,12 +262,22 @@ angular.module('Application').controller('BrandingController', ['$scope', '$loca
|
||||
return;
|
||||
}
|
||||
|
||||
Client.refreshConfig(function () {
|
||||
if ($scope.about.avatar) Client.resetAvatar();
|
||||
$scope.background.submit(function (error) {
|
||||
if (error) {
|
||||
$scope.about.busy = false;
|
||||
console.error('Unable to change background.', error);
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.aboutForm.$setPristine();
|
||||
$scope.about.avatar = null;
|
||||
$scope.about.busy = false;
|
||||
Client.refreshConfig(function () {
|
||||
if ($scope.about.avatar) Client.resetAvatar();
|
||||
|
||||
$scope.aboutForm.$setPristine();
|
||||
$scope.about.avatar = null;
|
||||
$scope.about.refresh();
|
||||
|
||||
$scope.about.busy = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,6 +98,25 @@
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.netcupApiPassword" name="netcupApiPassword" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'netcup'">
|
||||
</div>
|
||||
|
||||
<!-- OVH -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'ovh'">
|
||||
<label class="control-label" for="inputConfigureOvhEndpoint">{{ 'domains.domainDialog.ovhEndpoint' | tr }}</label>
|
||||
<select class="form-control" name="endpoint" id="inputConfigureOvhEndpoint" ng-model="domainConfigure.ovhEndpoint" ng-options="a.value as a.name for a in ovhEndpoints" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'ovh'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'ovh'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.ovhConsumerKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.ovhConsumerKey" name="ovhConsumerKey" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'ovh'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'ovh'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.ovhAppKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.ovhAppKey" name="ovhAppKey" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'ovh'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'ovh'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.ovhAppSecret' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.ovhAppSecret" name="ovhAppSecret" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'ovh'">
|
||||
</div>
|
||||
|
||||
<!-- Porkbun -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'porkbun'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.porkbunApikey' | tr }}</label>
|
||||
@@ -146,6 +165,12 @@
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.bunnyAccessKey" name="bunnyAccessKey" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'bunny'">
|
||||
</div>
|
||||
|
||||
<!-- dnsimple -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'dnsimple'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.dnsimpleAccessToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.dnsimpleAccessToken" name="dnsimpleAccessToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'dnsimple'">
|
||||
</div>
|
||||
|
||||
<!-- Hetzner -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'hetzner'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.hetznerToken' | tr }}</label>
|
||||
@@ -158,6 +183,12 @@
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.vultrToken" name="vultrToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'vultr'">
|
||||
</div>
|
||||
|
||||
<!-- deSEC -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'desec'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.deSecToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.deSecToken" name="deSecToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'desec'">
|
||||
</div>
|
||||
|
||||
<!-- Name.com -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecom'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.nameComUsername' | tr }}</label>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
/* global async */
|
||||
/* global angular */
|
||||
/* global $, TASK_TYPES */
|
||||
/* global $, TASK_TYPES, ENDPOINTS_OVH */
|
||||
|
||||
angular.module('Application').controller('DomainsController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
@@ -41,12 +41,14 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
{ name: 'Custom Wildcard Certificate', value: 'fallback' },
|
||||
];
|
||||
|
||||
// keep in sync with setupdns.js
|
||||
// keep in sync with setup.js
|
||||
$scope.dnsProvider = [
|
||||
{ name: 'AWS Route53', value: 'route53' },
|
||||
{ name: 'Bunny', value: 'bunny' },
|
||||
{ name: 'Cloudflare', value: 'cloudflare' },
|
||||
{ name: 'deSEC', value: 'desec' },
|
||||
{ name: 'DigitalOcean', value: 'digitalocean' },
|
||||
{ name: 'DNSimple', value: 'dnsimple' },
|
||||
{ name: 'Gandi LiveDNS', value: 'gandi' },
|
||||
{ name: 'GoDaddy', value: 'godaddy' },
|
||||
{ name: 'Google Cloud DNS', value: 'gcdns' },
|
||||
@@ -55,6 +57,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
{ name: 'Name.com', value: 'namecom' },
|
||||
{ name: 'Namecheap', value: 'namecheap' },
|
||||
{ name: 'Netcup', value: 'netcup' },
|
||||
{ name: 'OVH', value: 'ovh' },
|
||||
{ name: 'Porkbun', value: 'porkbun' },
|
||||
{ name: 'Vultr', value: 'vultr' },
|
||||
{ name: 'Wildcard', value: 'wildcard' },
|
||||
@@ -67,13 +70,16 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
case 'bunny': return 'Bunny';
|
||||
case 'route53': return 'AWS Route53';
|
||||
case 'cloudflare': return 'Cloudflare';
|
||||
case 'desec': return 'deSEC';
|
||||
case 'digitalocean': return 'DigitalOcean';
|
||||
case 'dnsimple': return 'dnsimple';
|
||||
case 'gandi': return 'Gandi LiveDNS';
|
||||
case 'hetzner': return 'Hetzner DNS';
|
||||
case 'linode': return 'Linode';
|
||||
case 'namecom': return 'Name.com';
|
||||
case 'namecheap': return 'Namecheap';
|
||||
case 'netcup': return 'Netcup';
|
||||
case 'ovh': return 'OVH';
|
||||
case 'gcdns': return 'Google Cloud';
|
||||
case 'godaddy': return 'GoDaddy';
|
||||
case 'vultr': return 'Vultr';
|
||||
@@ -85,6 +91,8 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
}
|
||||
};
|
||||
|
||||
$scope.ovhEndpoints = ENDPOINTS_OVH;
|
||||
|
||||
$scope.needsPort80 = function (dnsProvider, tlsProvider) {
|
||||
return ((dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') &&
|
||||
(tlsProvider === 'letsencrypt-prod' || tlsProvider === 'letsencrypt-staging'));
|
||||
@@ -249,8 +257,10 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
cloudflareTokenType: 'GlobalApiKey',
|
||||
linodeToken: '',
|
||||
bunnyAccessKey: '',
|
||||
dnsimpleAccessToken: '',
|
||||
hetznerToken: '',
|
||||
vultrToken: '',
|
||||
deSecToken: '',
|
||||
nameComToken: '',
|
||||
nameComUsername: '',
|
||||
namecheapUsername: '',
|
||||
@@ -258,6 +268,10 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
netcupCustomerNumber: '',
|
||||
netcupApiKey: '',
|
||||
netcupApiPassword: '',
|
||||
ovhEndpoint: 'ovh-eu',
|
||||
ovhConsumerKey: '',
|
||||
ovhAppKey: '',
|
||||
ovhAppSecret: '',
|
||||
porkbunSecretapikey: '',
|
||||
porkbunApikey: '',
|
||||
|
||||
@@ -307,8 +321,10 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.domainConfigure.digitalOceanToken = domain.provider === 'digitalocean' ? domain.config.token : '';
|
||||
$scope.domainConfigure.linodeToken = domain.provider === 'linode' ? domain.config.token : '';
|
||||
$scope.domainConfigure.bunnyAccessKey = domain.provider === 'bunny' ? domain.config.accessKey : '';
|
||||
$scope.domainConfigure.dnsimpleAccessToken = domain.provider === 'dnsimple' ? domain.config.accessToken : '';
|
||||
$scope.domainConfigure.hetznerToken = domain.provider === 'hetzner' ? domain.config.token : '';
|
||||
$scope.domainConfigure.vultrToken = domain.provider === 'vultr' ? domain.config.token : '';
|
||||
$scope.domainConfigure.deSecToken = domain.provider === 'desec' ? domain.config.token : '';
|
||||
$scope.domainConfigure.gandiApiKey = domain.provider === 'gandi' ? domain.config.token : '';
|
||||
$scope.domainConfigure.cloudflareToken = domain.provider === 'cloudflare' ? domain.config.token : '';
|
||||
$scope.domainConfigure.cloudflareEmail = domain.provider === 'cloudflare' ? domain.config.email : '';
|
||||
@@ -328,6 +344,11 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.domainConfigure.netcupApiKey = domain.provider === 'netcup' ? domain.config.apiKey : '';
|
||||
$scope.domainConfigure.netcupApiPassword = domain.provider === 'netcup' ? domain.config.apiPassword : '';
|
||||
|
||||
$scope.domainConfigure.ovhEndpoint = domain.provider === 'ovh' ? domain.config.endpoint : '';
|
||||
$scope.domainConfigure.ovhConsumerKey = domain.provider === 'ovh' ? domain.config.consumerKey : '';
|
||||
$scope.domainConfigure.ovhAppKey = domain.provider === 'ovh' ? domain.config.appKey : '';
|
||||
$scope.domainConfigure.ovhAppSecret = domain.provider === 'ovh' ? domain.config.appSecret : '';
|
||||
|
||||
$scope.domainConfigure.porkbunApikey = domain.provider === 'porkbun' ? domain.config.apikey : '';
|
||||
$scope.domainConfigure.porkbunSecretapikey = domain.provider === 'porkbun' ? domain.config.secretapikey : '';
|
||||
|
||||
@@ -379,10 +400,14 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
data.token = $scope.domainConfigure.linodeToken;
|
||||
} else if (provider === 'bunny') {
|
||||
data.accessKey = $scope.domainConfigure.bunnyAccessKey;
|
||||
} else if (provider === 'dnsimple') {
|
||||
data.accessToken = $scope.domainConfigure.dnsimpleAccessToken;
|
||||
} else if (provider === 'hetzner') {
|
||||
data.token = $scope.domainConfigure.hetznerToken;
|
||||
} else if (provider === 'vultr') {
|
||||
data.token = $scope.domainConfigure.vultrToken;
|
||||
} else if (provider === 'desec') {
|
||||
data.token = $scope.domainConfigure.deSecToken;
|
||||
} else if (provider === 'gandi') {
|
||||
data.token = $scope.domainConfigure.gandiApiKey;
|
||||
} else if (provider === 'godaddy') {
|
||||
@@ -403,6 +428,11 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
data.customerNumber = $scope.domainConfigure.netcupCustomerNumber;
|
||||
data.apiKey = $scope.domainConfigure.netcupApiKey;
|
||||
data.apiPassword = $scope.domainConfigure.netcupApiPassword;
|
||||
} else if (provider === 'ovh') {
|
||||
data.endpoint = $scope.domainConfigure.ovhEndpoint;
|
||||
data.consumerKey = $scope.domainConfigure.ovhConsumerKey;
|
||||
data.appKey = $scope.domainConfigure.ovhAppKey;
|
||||
data.appSecret = $scope.domainConfigure.ovhAppSecret;
|
||||
} else if (provider === 'porkbun') {
|
||||
data.apikey = $scope.domainConfigure.porkbunApikey;
|
||||
data.secretapikey = $scope.domainConfigure.porkbunSecretapikey;
|
||||
@@ -472,9 +502,14 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.domainConfigure.netcupCustomerNumber = '';
|
||||
$scope.domainConfigure.netcupApiKey = '';
|
||||
$scope.domainConfigure.netcupApiPassword = '';
|
||||
$scope.domainConfigure.ovhEndpoint = '';
|
||||
$scope.domainConfigure.ovhConsumerKey = '';
|
||||
$scope.domainConfigure.ovhAppKey = '';
|
||||
$scope.domainConfigure.ovhAppSecret = '';
|
||||
$scope.domainConfigure.porkbunApikey = '';
|
||||
$scope.domainConfigure.porkbunSecretapikey = '';
|
||||
$scope.domainConfigure.vultrToken = '';
|
||||
$scope.domainConfigure.deSecToken = '';
|
||||
|
||||
$scope.domainConfigure.tlsConfig.provider = 'letsencrypt-prod';
|
||||
$scope.domainConfigure.zoneName = '';
|
||||
|
||||
@@ -157,9 +157,10 @@
|
||||
{{ 'email.editMailboxDialog.enableStorageQuota' | tr }} <b ng-hide="!mailboxes.edit.storageQuotaEnabled">: {{ mailboxes.edit.storageQuota | prettyDecimalSize }}</b>
|
||||
</input>
|
||||
</label>
|
||||
<div style="padding: 0 10px;">
|
||||
<slider id="storageQuota" ng-disabled="!mailboxes.edit.storageQuotaEnabled" ng-model="mailboxes.edit.storageQuota" step="500000000" ticks-snap-bounds="1000000000" tooltip="hide" ticks="storageQuotaTicks"></slider>
|
||||
</div>
|
||||
<input type="range" id="storageQuota" ng-disabled="!mailboxes.edit.storageQuotaEnabled" ng-model="mailboxes.edit.storageQuota" step="500000000" min="{{ storageQuotaTicks[0] }}" max="{{ storageQuotaTicks[storageQuotaTicks.length-1] }}" list="storageQuotaTicks" />
|
||||
<datalist id="storageQuotaTicks">
|
||||
<option ng-repeat="quota in storageQuotaTicks" value="{{ quota }}"></option>
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
@@ -385,7 +386,7 @@
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a href="https://docs.cloudron.io/email/" target="_blank">{{ 'app.docsAction' | tr }}</a></li>
|
||||
<li ng-class="{ 'disabled': !domain.mailConfig.enabled }"><a href="" ng-click="howToConnectInfo.show()">{{ 'email.config.clientConfiguration' | tr }}</a></li>
|
||||
<li ng-class="{ 'disabled': !domain.mailConfig.enabled }"><a href="" ng-click="domain.mailConfig.enabled ? howToConnectInfo.show() : null">{{ 'email.config.clientConfiguration' | tr }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
|
||||
@@ -42,7 +42,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
$scope.domain = null;
|
||||
$scope.adminDomain = null;
|
||||
$scope.mailUsage = null;
|
||||
$scope.storageQuotaTicks = [ 500*1000*1000, 1*1000*1000*1000, 15*1000*1000*1000, 50*1000*1000*1000, 100*1000*1000*1000 ];
|
||||
$scope.storageQuotaTicks = [ 500*1000*1000, 5*1000*1000*1000, 15*1000*1000*1000, 50*1000*1000*1000, 100*1000*1000*1000 ];
|
||||
|
||||
$scope.expectedDnsRecords = {
|
||||
mx: { },
|
||||
@@ -639,7 +639,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
ownerType: $scope.mailboxes.edit.owner.type,
|
||||
active: $scope.mailboxes.edit.active,
|
||||
enablePop3: $scope.mailboxes.edit.enablePop3,
|
||||
storageQuota: $scope.mailboxes.edit.storageQuotaEnabled ? $scope.mailboxes.edit.storageQuota : 0,
|
||||
storageQuota: $scope.mailboxes.edit.storageQuotaEnabled ? parseInt($scope.mailboxes.edit.storageQuota) : 0,
|
||||
messagesQuota: 0
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
/* global angular */
|
||||
|
||||
angular.module('Application').controller('EmailsEventlogController', ['$scope', '$location', '$translate', '$timeout', 'Client', function ($scope, $location, $translate, $timeout, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastOwner) $location.path('/'); });
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.ready = false;
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
/* global angular */
|
||||
|
||||
angular.module('Application').controller('EmailsQueueController', ['$scope', '$location', '$translate', '$timeout', 'Client', function ($scope, $location, $translate, $timeout, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastOwner) $location.path('/'); });
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.ready = false;
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
<br>
|
||||
<form name="maxEmailSizeChangeForm" role="form" novalidate ng-submit="maxEmailSize.submit()" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'emails.changeMailSizeDialog.size' | tr }} <b>{{ maxEmailSize.size | prettyDecimalSize }}</b></label>
|
||||
<slider ng-model="maxEmailSize.size" tooltip="hide" min="1000000" max="1000000000" step="1000000"></slider>
|
||||
<label class="control-label" for="maxEmailSizeInput">{{ 'emails.changeMailSizeDialog.size' | tr }} <b>{{ maxEmailSize.size | prettyDecimalSize }}</b></label>
|
||||
<input type="range" id="maxEmailSizeInput" ng-model="maxEmailSize.size" step="1000000" min="1000000" max="1000000000" />
|
||||
</div>
|
||||
<input class="ng-hide" type="submit"/>
|
||||
</form>
|
||||
@@ -101,8 +101,8 @@
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'emails.spamFilterDialog.blacklisteAddresses' | tr }}</label>
|
||||
<p class="small">{{ 'emails.spamFilterDialog.blacklisteAddressesInfo' | tr }}</p>
|
||||
<div class="has-error" ng-show="spamConfig.error.blacklist">{{ spamConfig.error.blacklist }}</div>
|
||||
<textarea ng-model="spamConfig.blacklist" placeholder="{{ 'emails.spamFilterDialog.blacklisteAddressesPlaceholder' | tr }}" name="blacklist" class="form-control" ng-class="{ 'has-error': !spamConfigChangeForm.blacklist.$dirty && spamConfig.error.blacklist }" rows="4"></textarea>
|
||||
<div class="has-error" ng-show="spamConfig.error.blocklist">{{ spamConfig.error.blocklist }}</div>
|
||||
<textarea ng-model="spamConfig.blocklist" placeholder="{{ 'emails.spamFilterDialog.blacklisteAddressesPlaceholder' | tr }}" name="blocklist" class="form-control" ng-class="{ 'has-error': !spamConfigChangeForm.blocklist.$dirty && spamConfig.error.blocklist }" rows="4"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'emails.spamFilterDialog.customRules' | tr }} <sup><a ng-href="https://docs.cloudron.io/email/#custom-spam-filtering-rules" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
@@ -156,8 +156,8 @@
|
||||
{{ 'emails.title' | tr }}
|
||||
|
||||
<div class="pull-right">
|
||||
<a class="btn btn-default" ng-show="user.isAtLeastOwner" href="#/emails-queue">{{ 'emails.action.queue' | tr }}</a>
|
||||
<a class="btn btn-default" ng-show="user.isAtLeastOwner" href="#/emails-eventlog">{{ 'eventlog.title' | tr }}</a>
|
||||
<a class="btn btn-default" ng-show="user.isAtLeastAdmin" href="#/emails-queue">{{ 'emails.action.queue' | tr }}</a>
|
||||
<a class="btn btn-default" ng-show="user.isAtLeastAdmin" href="#/emails-eventlog">{{ 'eventlog.title' | tr }}</a>
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
@@ -195,10 +195,20 @@
|
||||
</td>
|
||||
<td class="elide-table-cell no-padding">
|
||||
<a href="/#/email/{{ domain.domain }}" class="email-domain-list-item">
|
||||
<span ng-show="domain.inbound && domain.outbound && domain.usage === null">{{ 'main.loadingPlaceholder' | tr }} ...</span>
|
||||
<span ng-show="domain.inbound && domain.outbound && domain.usage !== null">{{ 'emails.domains.stats' | tr:{ mailboxCount: domain.mailboxCount, usage: (domain.usage | prettyDecimalSize) } }}</span>
|
||||
<span ng-show="!domain.inbound && domain.outbound">{{ 'emails.domains.outbound' | tr }}</span>
|
||||
<span ng-show="!domain.inbound && !domain.outbound">{{ 'emails.domains.disabled' | tr }}</span>
|
||||
<span ng-switch on="domain.loading">
|
||||
<span ng-switch-when="true">{{ 'main.loadingPlaceholder' | tr }} ...</span>
|
||||
<span ng-switch-default>
|
||||
<span ng-switch on="domain.inbound">
|
||||
<span ng-switch-when="true">
|
||||
<span ng-show="domain.loadingUsage">{{ 'emails.domains.stats' | tr:{ mailboxCount: domain.mailboxCount } }} {{ 'main.loadingPlaceholder' | tr }} ... </span>
|
||||
<span ng-show="!domain.loadingUsage">{{ 'emails.domains.stats' | tr:{ mailboxCount: domain.mailboxCount, usage: (domain.usage | prettyDecimalSize) } }}</span>
|
||||
</span>
|
||||
<span ng-switch-default>
|
||||
<span ng-show="domain.outbound">{{ 'emails.domains.outbound' | tr }}</span>
|
||||
<span ng-show="!domain.outbound">{{ 'emails.domains.disabled' | tr }}</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right no-wrap">
|
||||
@@ -213,11 +223,11 @@
|
||||
</div>
|
||||
|
||||
<!-- mailbox sharing -->
|
||||
<div class="text-left section-header" ng-show="user.isAtLeastOwner">
|
||||
<div class="text-left section-header" ng-show="user.isAtLeastAdmin">
|
||||
<h3>{{ 'emails.mailboxSharing.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" ng-show="user.isAtLeastOwner" style="margin-bottom: 15px;">
|
||||
<div class="card" ng-show="user.isAtLeastAdmin" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>{{ 'emails.mailboxSharing.description' | tr }}</p>
|
||||
@@ -234,7 +244,7 @@
|
||||
</div>
|
||||
|
||||
<!-- server location -->
|
||||
<div class="text-left section-header" ng-show="user.isAtLeastOwner">
|
||||
<div class="text-left section-header" ng-show="user.isAtLeastAdmin">
|
||||
<h3>
|
||||
{{ 'emails.settings.location' | tr }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
@@ -252,7 +262,7 @@
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card" ng-show="user.isAtLeastAdmin">
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<p ng-bind-html="'emails.changeDomainDialog.description' | tr"></p>
|
||||
@@ -302,11 +312,11 @@
|
||||
</div>
|
||||
|
||||
<!-- settings -->
|
||||
<div class="text-left section-header" ng-show="user.isAtLeastOwner">
|
||||
<div class="text-left section-header" ng-show="user.isAtLeastAdmin">
|
||||
<h3>{{ 'emails.settings.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" ng-show="user.isAtLeastOwner" style="margin-bottom: 15px;">
|
||||
<div class="card" ng-show="user.isAtLeastAdmin" style="margin-bottom: 15px;">
|
||||
<p ng-bind-html=" 'emails.settings.info' | tr "></p>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
@@ -331,7 +341,7 @@
|
||||
<span class="text-muted">{{ 'emails.settings.spamFilter' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ 'emails.settings.spamFilterOverview' | tr:{ blacklistCount: spamConfig.acl.blacklist.length } }} <a href="" ng-click="spamConfig.show()"><i class="fa fa-edit text-small"></i></a></span>
|
||||
<span>{{ 'emails.settings.spamFilterOverview' | tr:{ blacklistCount: spamConfig.acl.blocklist.length } }} <a href="" ng-click="spamConfig.show()"><i class="fa fa-edit text-small"></i></a></span>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'emails.settings.solrFts' | tr }}</span>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
/* global $, angular, TASK_TYPES */
|
||||
/* global async */
|
||||
|
||||
angular.module('Application').controller('EmailsController', ['$scope', '$location', '$translate', '$timeout', 'Client', function ($scope, $location, $translate, $timeout, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastMailManager) $location.path('/'); });
|
||||
@@ -78,7 +79,7 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati
|
||||
$scope.mailLocation.message = '';
|
||||
$scope.mailLocation.errorMessage = '';
|
||||
|
||||
Client.setMailLocation($scope.mailLocation.subdomain, $scope.mailLocation.domain.domain, function (error, result) {
|
||||
Client.setMailLocation($scope.mailLocation.subdomain, $scope.mailLocation.domain.domain, function (error) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
$scope.mailLocation.errorMessage = error.message;
|
||||
@@ -120,7 +121,7 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati
|
||||
submit: function () {
|
||||
$scope.maxEmailSize.busy = true;
|
||||
|
||||
Client.setMaxEmailSize($scope.maxEmailSize.size, function (error) {
|
||||
Client.setMaxEmailSize(parseInt($scope.maxEmailSize.size), function (error) {
|
||||
$scope.maxEmailSize.busy = false;
|
||||
|
||||
if (error) return console.error(error);
|
||||
@@ -249,11 +250,11 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati
|
||||
$scope.spamConfig = {
|
||||
busy: false,
|
||||
error: {},
|
||||
acl: { whitelist: [], blacklist: [] },
|
||||
acl: { allowlist: [], blocklist: [] },
|
||||
customConfig: '',
|
||||
|
||||
config: '',
|
||||
blacklist: '', // currently, we don't support whitelist because it requires user to understand a bit more of what he is doing
|
||||
blocklist: '', // currently, we don't support allowlist because it requires user to understand a bit more of what he is doing
|
||||
|
||||
refresh: function () {
|
||||
Client.getSpamCustomConfig(function (error, config) {
|
||||
@@ -273,7 +274,7 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati
|
||||
$scope.spamConfig.busy = false;
|
||||
$scope.spamConfig.error = {};
|
||||
|
||||
$scope.spamConfig.blacklist = $scope.spamConfig.acl.blacklist.join('\n');
|
||||
$scope.spamConfig.blocklist = $scope.spamConfig.acl.blocklist.join('\n');
|
||||
$scope.spamConfig.config = $scope.spamConfig.customConfig;
|
||||
|
||||
$scope.spamConfigChangeForm.$setUntouched();
|
||||
@@ -286,13 +287,13 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati
|
||||
$scope.spamConfig.busy = true;
|
||||
$scope.spamConfig.error = {};
|
||||
|
||||
var blacklist = $scope.spamConfig.blacklist.split('\n').filter(function (l) { return l !== ''; });
|
||||
var blocklist = $scope.spamConfig.blocklist.split('\n').filter(function (l) { return l !== ''; });
|
||||
|
||||
Client.setSpamAcl({ blacklist: blacklist, whitelist: [] }, function (error) {
|
||||
Client.setSpamAcl({ blocklist: blocklist, allowlist: [] }, function (error) {
|
||||
if (error) {
|
||||
$scope.spamConfig.busy = false;
|
||||
$scope.spamConfig.error.blacklist = error.message;
|
||||
$scope.spamConfigChangeForm.blacklist.$setPristine();
|
||||
$scope.spamConfig.error.blocklist = error.message;
|
||||
$scope.spamConfigChangeForm.blocklist.$setPristine();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -404,44 +405,83 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati
|
||||
}
|
||||
};
|
||||
|
||||
function refreshDomainStatuses() {
|
||||
$scope.domains.forEach(function (domain) {
|
||||
domain.usage = null; // used by ui to show 'loading'
|
||||
function refreshMailStatus(domain, done) {
|
||||
Client.getMailStatusForDomain(domain.domain, function (error, result) {
|
||||
if (error) {
|
||||
console.error('Failed to fetch mail status for domain', domain.domain, error);
|
||||
return done();
|
||||
}
|
||||
|
||||
Client.getMailStatusForDomain(domain.domain, function (error, result) {
|
||||
if (error) return console.error('Failed to fetch mail status for domain', domain.domain, error);
|
||||
domain.status = result;
|
||||
|
||||
domain.status = result;
|
||||
domain.statusOk = Object.keys(result).every(function (k) {
|
||||
if (k === 'dns') return Object.keys(result.dns).every(function (k) { return result.dns[k].status; });
|
||||
|
||||
domain.statusOk = Object.keys(result).every(function (k) {
|
||||
if (k === 'dns') return Object.keys(result.dns).every(function (k) { return result.dns[k].status; });
|
||||
if (!('status' in result[k])) return true; // if status is not present, the test was not run
|
||||
|
||||
if (!('status' in result[k])) return true; // if status is not present, the test was not run
|
||||
|
||||
return result[k].status;
|
||||
});
|
||||
return result[k].status;
|
||||
});
|
||||
|
||||
Client.getMailConfigForDomain(domain.domain, function (error, mailConfig) {
|
||||
if (error) return console.error('Failed to fetch mail config for domain', domain.domain, error);
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
domain.inbound = mailConfig.enabled;
|
||||
domain.outbound = mailConfig.relay.provider !== 'noop';
|
||||
function refreshMailConfig(domain, done) {
|
||||
Client.getMailConfigForDomain(domain.domain, function (error, mailConfig) {
|
||||
if (error) {
|
||||
console.error('Failed to fetch mail config for domain', domain.domain, error);
|
||||
return done();
|
||||
}
|
||||
|
||||
// do this even if no outbound since people forget to remove mailboxes
|
||||
Client.getMailboxCount(domain.domain, function (error, count) {
|
||||
if (error) return console.error('Failed to fetch mailboxes for domain', domain.domain, error);
|
||||
domain.inbound = mailConfig.enabled;
|
||||
domain.outbound = mailConfig.relay.provider !== 'noop';
|
||||
|
||||
domain.mailboxCount = count;
|
||||
// do this even if no outbound since people forget to remove mailboxes
|
||||
Client.getMailboxCount(domain.domain, function (error, count) {
|
||||
if (error) {
|
||||
console.error('Failed to fetch mailboxes for domain', domain.domain, error);
|
||||
return done();
|
||||
}
|
||||
|
||||
Client.getMailUsage(domain.domain, function (error, usage) {
|
||||
if (error) return console.error('Failed to fetch usage for domain', domain.domain, error);
|
||||
domain.mailboxCount = count;
|
||||
|
||||
domain.usage = 0;
|
||||
// we used to use quotaValue here but it's quite different wrt diskSize. so choose diskSize consistently
|
||||
// also, quotaValue can be missing for deleted mailboxes that are on disk but removed from dovecot/ldap itself
|
||||
Object.keys(usage).forEach(function (m) { domain.usage += usage[m].diskSize; });
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function refreshMailUsage(domain, done) {
|
||||
Client.getMailUsage(domain.domain, function (error, usage) {
|
||||
if (error) {
|
||||
console.error('Failed to fetch usage for domain', domain.domain, error);
|
||||
return done();
|
||||
}
|
||||
|
||||
domain.usage = 0;
|
||||
// we used to use quotaValue here but it's quite different wrt diskSize. so choose diskSize consistently
|
||||
// also, quotaValue can be missing for deleted mailboxes that are on disk but removed from dovecot/ldap itself
|
||||
Object.keys(usage).forEach(function (m) { domain.usage += usage[m].diskSize; });
|
||||
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
function refreshDomainStatuses() {
|
||||
async.each($scope.domains, function (domain, iteratorDone) {
|
||||
async.series([
|
||||
refreshMailStatus.bind(null, domain),
|
||||
refreshMailConfig.bind(null, domain),
|
||||
], function () {
|
||||
domain.loading = false;
|
||||
iteratorDone();
|
||||
});
|
||||
}, function () {
|
||||
// mail usage is loaded separately with a cancellation check. when there are a lot of domains, it runs a long time in background and slows down loading of new views
|
||||
async.eachLimit($scope.domains, 5, function (domain, itemDone) {
|
||||
if ($scope.$$destroyed) return itemDone(); // abort!
|
||||
refreshMailUsage(domain, function () {
|
||||
domain.loadingUsage = false;
|
||||
itemDone();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -451,10 +491,12 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati
|
||||
Client.getDomains(function (error, domains) {
|
||||
if (error) return console.error('Unable to get domain listing.', error);
|
||||
|
||||
domains.forEach(function (domain) { domain.loading = true; domain.loadingUsage = true; }); // used by ui to show 'loading'
|
||||
$scope.domains = domains;
|
||||
|
||||
$scope.ready = true;
|
||||
|
||||
if ($scope.user.isAtLeastOwner) {
|
||||
if ($scope.user.isAtLeastAdmin) {
|
||||
$scope.mailLocation.refresh();
|
||||
$scope.maxEmailSize.refresh();
|
||||
$scope.virtualAllMail.refresh();
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-md-2">{{ 'eventlog.time' | tr }}</th>
|
||||
<th class="col-md-3">{{ 'eventlog.source' | tr }}</th>
|
||||
<th class="col-md-7">{{ 'eventlog.details' | tr }}</th>
|
||||
<th class="col-md-2">{{ 'eventlog.source' | tr }}</th>
|
||||
<th class="col-md-8">{{ 'eventlog.details' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody ng-repeat="eventLog in eventLogs">
|
||||
@@ -43,7 +43,10 @@
|
||||
<td ng-bind-html="eventLog.details"></td>
|
||||
</tr>
|
||||
<tr ng-show="activeEventLog === eventLog">
|
||||
<td colspan="4"><pre class="eventlog-details">{{ eventLog.raw.data | json }}</pre></td>
|
||||
<td colspan="4">
|
||||
<p ng-show="eventLog.raw.source.ip">Source IP: <code>{{ eventLog.raw.source.ip }}</code></p>
|
||||
<pre class="eventlog-details">{{ eventLog.raw.data | json }}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -46,10 +46,12 @@ angular.module('Application').controller('EventLogController', ['$scope', '$loca
|
||||
{ name: 'cloudron.update', value: 'cloudron.update' },
|
||||
{ name: 'cloudron.update.finish', value: 'cloudron.update.finish' },
|
||||
{ name: 'dashboard.domain.update', value: 'dashboard.domain.update' },
|
||||
{ name: 'directoryserver.configure', value: 'directoryserver.configure' },
|
||||
{ name: 'dyndns.update', value: 'dyndns.update' },
|
||||
{ name: 'domain.add', value: 'domain.add' },
|
||||
{ name: 'domain.update', value: 'domain.update' },
|
||||
{ name: 'domain.remove', value: 'domain.remove' },
|
||||
{ name: 'externalldap.configure', value: 'externalldap.configure' },
|
||||
{ name: 'mail.location', value: 'mail.location' },
|
||||
{ name: 'mail.enabled', value: 'mail.enabled' },
|
||||
{ name: 'mail.box.add', value: 'mail.box.add' },
|
||||
@@ -70,6 +72,7 @@ angular.module('Application').controller('EventLogController', ['$scope', '$loca
|
||||
{ name: 'user.remove', value: 'user.remove' },
|
||||
{ name: 'user.transfer', value: 'user.transfer' },
|
||||
{ name: 'user.update', value: 'user.update' },
|
||||
{ name: 'userdirectory.profileconfig.update', value: 'userdirectory.profileconfig.update '},
|
||||
{ name: 'volume.add', value: 'volume.add' },
|
||||
{ name: 'volume.update', value: 'volume.update' },
|
||||
{ name: 'volume.remove', value: 'volume.update' },
|
||||
@@ -140,6 +143,7 @@ angular.module('Application').controller('EventLogController', ['$scope', '$loca
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
$scope.search = $location.search().search || ''; // sent from the backups view when app is deleted
|
||||
fetchEventLogs();
|
||||
});
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="blocklist.submit()"><i class="fa fa-circle-notch fa-spin" ng-show="blocklist.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-disabled="blocklist.busy" ng-click="blocklist.submit()"><i class="fa fa-circle-notch fa-spin" ng-show="blocklist.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,7 +91,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="trustedIps.submit()"><i class="fa fa-circle-notch fa-spin" ng-show="trustedIps.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-disabled="trustedIps.busy" ng-click="trustedIps.submit()"><i class="fa fa-circle-notch fa-spin" ng-show="trustedIps.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -171,7 +171,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row" ng-show="sysinfo.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'network.ip.address' | tr }}</span>
|
||||
</div>
|
||||
@@ -251,11 +251,11 @@
|
||||
</div>
|
||||
|
||||
<!-- Firewall -->
|
||||
<div class="text-left section-header" ng-show="user.isAtLeastOwner">
|
||||
<div class="text-left section-header">
|
||||
<h3>{{ 'network.firewall.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" ng-show="user.isAtLeastOwner">
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'network.firewall.blockedIpRanges' | tr }}</span>
|
||||
|
||||
@@ -11,6 +11,7 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
|
||||
|
||||
// keep in sync with sysinfo.js
|
||||
$scope.sysinfoProvider = [
|
||||
{ name: 'Disabled', value: 'noop' },
|
||||
{ name: 'Public IP', value: 'generic' },
|
||||
{ name: 'Static IP Address', value: 'fixed' },
|
||||
{ name: 'Network Interface', value: 'network-interface' }
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card notification-item" ng-repeat="notification in notifications" ng-class="{'notification-unread': !notification.acknowledged }" ng-click="notification.isCollapsed = !notification.isCollapsed" ng-style="{ borderLeftColor: (notification | notificadtionTypeToColor) }">
|
||||
<div class="card notification-item" ng-repeat="notification in notifications" ng-class="{'notification-unread': !notification.acknowledged }" ng-click="notification.isCollapsed = !notification.isCollapsed" ng-style="{ borderLeftColor: (notification | notificationTypeToColor) }">
|
||||
<div class="row">
|
||||
<div class="col-xs-12" ng-class="{ 'notification-details': notification.detailsShown }">
|
||||
<span class="notification-title">{{ notification.title }}</span> <small class="text-muted" uib-tooltip="{{ notification.creationTime | prettyLongDate }}">{{ notification.creationTime | prettyDate }}</small>
|
||||
|
||||
@@ -115,13 +115,21 @@
|
||||
<h4 class="modal-title">{{ 'profile.changeEmail.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="emailChangeForm" role="form" novalidate ng-submit="emailchange.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (emailChangeForm.email.$dirty && emailChangeForm.email.$invalid) || (!emailChangeForm.email.$dirty && emailchange.error.email)}">
|
||||
<input type="email" class="form-control" ng-model="emailchange.email" id="inputEmailChangeEmail" name="email" required autofocus>
|
||||
<div class="control-label" ng-show="(!emailChangeForm.email.$dirty && emailchange.error.email) || (emailChangeForm.email.$dirty && emailChangeForm.email.$invalid)">
|
||||
<form name="emailChangeForm" role="form" novalidate ng-submit="emailChange.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (emailChangeForm.email.$dirty && emailChangeForm.email.$invalid) || (!emailChangeForm.email.$dirty && emailChange.error.email)}">
|
||||
<label class="control-label" for="inputEmailChangeEmail">{{ 'profile.changeEmail.email' | tr }}</label>
|
||||
<input type="email" class="form-control" ng-model="emailChange.email" id="inputEmailChangeEmail" name="email" required autofocus>
|
||||
<div class="control-label" ng-show="(!emailChangeForm.email.$dirty && emailChange.error.email) || (emailChangeForm.email.$dirty && emailChangeForm.email.$invalid)">
|
||||
<small ng-show="emailChangeForm.email.$error.required">{{ 'profile.changeEmail.errorEmailRequired' | tr }}</small>
|
||||
<small ng-show="(emailChangeForm.email.$dirty && emailChangeForm.email.$invalid) && !emailChangeForm.email.$error.required">{{ 'profile.changeEmail.errorEmailInvalid' | tr }}</small>
|
||||
<small ng-show="!emailChangeForm.email.$dirty && emailchange.error.email">{{ emailchange.error.email }}</small>
|
||||
<small ng-show="!emailChangeForm.email.$dirty && emailChange.error.email">{{ emailChange.error.email }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (emailChange.error.password && !emailChangeForm.password.$dirty) }">
|
||||
<label class="control-label" for="inputEmailChangePassword">{{ 'profile.changeEmail.password' | tr }}</label>
|
||||
<input type="password" class="form-control" ng-model="emailChange.password" id="inputEmailChangePassword" name="password" required autofocus password-reveal>
|
||||
<div class="control-label" ng-show="emailChange.error.password && !emailChangeForm.password.$dirty">
|
||||
<small ng-show="emailChange.error.password">{{ 'profile.changeEmail.errorWrongPassword' | tr }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="emailChangeForm.$invalid"/>
|
||||
@@ -129,7 +137,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="emailchange.submit()" ng-disabled="emailChangeForm.$invalid || emailchange.busy"><i class="fa fa-circle-notch fa-spin" ng-show="emailchange.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="emailChange.submit()" ng-disabled="emailChangeForm.$invalid || emailChange.busy"><i class="fa fa-circle-notch fa-spin" ng-show="emailChange.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -387,8 +395,8 @@
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-3" style="min-width: 150px;">
|
||||
<div class="settings-avatar" style="background-image: url('{{ user.avatarUrl }}');">
|
||||
<div class="overlay" ng-click="avatarChange.showChangeAvatar()"></div>
|
||||
<div class="settings-avatar" style="background-image: url('{{ user.avatarUrl }}');" ng-click="avatarChange.showChangeAvatar()">
|
||||
<i class="picture-edit-indicator fa fa-pencil-alt"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-9">
|
||||
@@ -408,7 +416,7 @@
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">{{ 'profile.primaryEmail' | tr }}</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">
|
||||
{{ user.email }} <a href="" ng-click="emailchange.show()" ng-hide="user.source || config.profileLocked"><i class="fa fa-edit text-small"></i></a>
|
||||
{{ user.email }} <a href="" ng-click="emailChange.show()" ng-hide="user.source || config.profileLocked"><i class="fa fa-edit text-small"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -417,7 +425,7 @@
|
||||
{{ user.fallbackEmail }} <a href="" ng-click="fallbackEmailChange.show()" ng-hide="user.source || config.profileLocked"><i class="fa fa-edit text-small"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr ng-hide="user.source">
|
||||
<td colspan="2" class="text-right">
|
||||
<a href="" ng-click="sendPasswordReset()">{{ 'profile.passwordResetAction' | tr }}</a>
|
||||
</td>
|
||||
@@ -437,7 +445,7 @@
|
||||
<br/>
|
||||
<button class="btn btn-default" ng-click="backgroundImageChange.show()">Set Background Image</button>
|
||||
<button class="btn btn-primary pull-right" ng-click="passwordchange.show()" ng-hide="user.source">{{ 'profile.changePasswordAction' | tr }}</button>
|
||||
<button class="btn pull-right" uib-tooltip="{{ user.source ? ('profile.enable2FANotAvailable' | tr) : '' }}" ng-disabled="user.source" ng-class="user.twoFactorAuthenticationEnabled ? 'btn-danger' : 'btn-success'" ng-click="twoFactorAuthentication.show()">{{ user.twoFactorAuthenticationEnabled ? 'profile.disable2FAAction' : 'profile.enable2FAAction' | tr }}</button> </div>
|
||||
<button class="btn pull-right" uib-tooltip="{{ (user.source && config.external2FA) ? ('profile.enable2FANotAvailable' | tr) : '' }}" ng-disabled="user.source && config.external2FA" ng-class="user.twoFactorAuthenticationEnabled ? 'btn-danger' : 'btn-success'" ng-click="twoFactorAuthentication.show()">{{ user.twoFactorAuthenticationEnabled ? 'profile.disable2FAAction' : 'profile.enable2FAAction' | tr }}</button> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,12 @@ angular.module('Application').controller('ProfileController', ['$scope', '$trans
|
||||
|
||||
$scope.$watch('language', function (newVal, oldVal) {
|
||||
if (newVal === oldVal) return;
|
||||
$translate.use(newVal.id);
|
||||
|
||||
Client.setProfileLanguage(newVal.id, function (error) {
|
||||
if (error) return console.error('Failed to reset password:', error);
|
||||
});
|
||||
|
||||
$translate.use(newVal.id); // this switches the language and saves locally in localStorage['NG_TRANSLATE_LANG_KEY']
|
||||
});
|
||||
|
||||
$scope.sendPasswordReset = function () {
|
||||
@@ -101,7 +106,7 @@ angular.module('Application').controller('ProfileController', ['$scope', '$trans
|
||||
return;
|
||||
}
|
||||
|
||||
Client.refreshUserInfo();
|
||||
Client.refreshProfile();
|
||||
|
||||
$('#twoFactorAuthenticationEnableModal').modal('hide');
|
||||
});
|
||||
@@ -123,7 +128,7 @@ angular.module('Application').controller('ProfileController', ['$scope', '$trans
|
||||
return;
|
||||
}
|
||||
|
||||
Client.refreshUserInfo();
|
||||
Client.refreshProfile();
|
||||
|
||||
$('#twoFactorAuthenticationDisableModal').modal('hide');
|
||||
});
|
||||
@@ -180,7 +185,7 @@ angular.module('Application').controller('ProfileController', ['$scope', '$trans
|
||||
function done(error) {
|
||||
if (error) return console.error('Unable to change avatar.', error);
|
||||
|
||||
Client.refreshUserInfo(function (error) {
|
||||
Client.refreshProfile(function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$('#avatarChangeModal').modal('hide');
|
||||
@@ -207,15 +212,10 @@ angular.module('Application').controller('ProfileController', ['$scope', '$trans
|
||||
avatarChangeReset: function () {
|
||||
$scope.avatarChange.error.avatar = null;
|
||||
|
||||
if ($scope.user.avatarUrl.indexOf('/api/v1/profile/avatar') !== -1) {
|
||||
$scope.avatarChange.type = 'custom';
|
||||
} else if ($scope.user.avatarUrl.indexOf('https://www.gravatar.com') === 0) {
|
||||
$scope.avatarChange.type = 'gravatar';
|
||||
} else {
|
||||
$scope.avatarChange.type = '';
|
||||
}
|
||||
|
||||
console.log($scope.user)
|
||||
$scope.avatarChange.type = $scope.user.avatarType;
|
||||
$scope.avatarChange.typeOrig = $scope.avatarChange.type;
|
||||
|
||||
document.getElementById('previewAvatar').src = $scope.avatarChange.type === 'custom' ? $scope.user.avatarUrl : '';
|
||||
$scope.avatarChange.pictureChanged = false;
|
||||
$scope.avatarChange.avatar = null;
|
||||
@@ -354,42 +354,44 @@ angular.module('Application').controller('ProfileController', ['$scope', '$trans
|
||||
}
|
||||
};
|
||||
|
||||
$scope.emailchange = {
|
||||
$scope.emailChange = {
|
||||
busy: false,
|
||||
error: {},
|
||||
email: '',
|
||||
password: '',
|
||||
|
||||
reset: function () {
|
||||
$scope.emailchange.busy = false;
|
||||
$scope.emailchange.error.email = null;
|
||||
$scope.emailchange.email = '';
|
||||
$scope.emailChange.busy = false;
|
||||
$scope.emailChange.error = {};
|
||||
$scope.emailChange.email = '';
|
||||
$scope.emailChange.password = '';
|
||||
|
||||
$scope.emailChangeForm.$setUntouched();
|
||||
$scope.emailChangeForm.$setPristine();
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.emailchange.reset();
|
||||
$scope.emailChange.reset();
|
||||
$('#emailChangeModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.emailchange.error.email = null;
|
||||
$scope.emailchange.busy = true;
|
||||
$scope.emailChange.error.email = null;
|
||||
$scope.emailChange.busy = true;
|
||||
|
||||
var data = {
|
||||
email: $scope.emailchange.email
|
||||
};
|
||||
|
||||
Client.updateProfile(data, function (error) {
|
||||
$scope.emailchange.busy = false;
|
||||
Client.setProfileEmail($scope.emailChange.email, $scope.emailChange.password, function (error) {
|
||||
$scope.emailChange.busy = false;
|
||||
|
||||
if (error) {
|
||||
if (error.statusCode === 409) $scope.emailchange.error.email = 'Email already taken';
|
||||
else if (error.statusCode === 400) $scope.emailchange.error.email = error.message;
|
||||
else console.error('Unable to change email.', error);
|
||||
|
||||
$('#inputEmailChangeEmail').focus();
|
||||
if (error.statusCode === 412) {
|
||||
$scope.emailChange.error.password = true;
|
||||
$scope.emailChange.password = '';
|
||||
$scope.emailChangeForm.password.$setPristine();
|
||||
$('#inputFallbackEmailChangePassword').focus();
|
||||
} else {
|
||||
$scope.emailChange.error.email = error.message;
|
||||
$('#inputEmailChangeEmail').focus();
|
||||
}
|
||||
|
||||
$scope.emailChangeForm.$setUntouched();
|
||||
$scope.emailChangeForm.$setPristine();
|
||||
@@ -397,9 +399,9 @@ angular.module('Application').controller('ProfileController', ['$scope', '$trans
|
||||
return;
|
||||
}
|
||||
|
||||
Client.refreshUserInfo();
|
||||
Client.refreshProfile();
|
||||
|
||||
$scope.emailchange.reset();
|
||||
$scope.emailChange.reset();
|
||||
$('#emailChangeModal').modal('hide');
|
||||
});
|
||||
}
|
||||
@@ -436,12 +438,7 @@ angular.module('Application').controller('ProfileController', ['$scope', '$trans
|
||||
$scope.fallbackEmailChange.error.generic = null;
|
||||
$scope.fallbackEmailChange.busy = true;
|
||||
|
||||
var data = {
|
||||
fallbackEmail: $scope.fallbackEmailChange.email,
|
||||
password: $scope.fallbackEmailChange.password
|
||||
};
|
||||
|
||||
Client.updateProfile(data, function (error) {
|
||||
Client.setProfileFallbackEmail($scope.fallbackEmailChange.email, $scope.fallbackEmailChange.password, function (error) {
|
||||
$scope.fallbackEmailChange.busy = false;
|
||||
|
||||
if (error) {
|
||||
@@ -460,7 +457,7 @@ angular.module('Application').controller('ProfileController', ['$scope', '$trans
|
||||
}
|
||||
|
||||
// update user info in the background
|
||||
Client.refreshUserInfo();
|
||||
Client.refreshProfile();
|
||||
|
||||
$scope.fallbackEmailChange.reset();
|
||||
$('#fallbackEmailChangeModal').modal('hide');
|
||||
@@ -592,11 +589,7 @@ angular.module('Application').controller('ProfileController', ['$scope', '$trans
|
||||
$scope.displayNameChange.error.displayName = null;
|
||||
$scope.displayNameChange.busy = true;
|
||||
|
||||
var user = {
|
||||
displayName: $scope.displayNameChange.displayName
|
||||
};
|
||||
|
||||
Client.updateProfile(user, function (error) {
|
||||
Client.setProfileDisplayName($scope.displayNameChange.displayName, function (error) {
|
||||
$scope.displayNameChange.busy = false;
|
||||
|
||||
if (error) {
|
||||
@@ -612,7 +605,7 @@ angular.module('Application').controller('ProfileController', ['$scope', '$trans
|
||||
}
|
||||
|
||||
// update user info in the background
|
||||
Client.refreshUserInfo();
|
||||
Client.refreshProfile();
|
||||
|
||||
$scope.displayNameChange.reset();
|
||||
$('#displayNameChangeModal').modal('hide');
|
||||
@@ -714,7 +707,7 @@ angular.module('Application').controller('ProfileController', ['$scope', '$trans
|
||||
Client.onReady(function () {
|
||||
$scope.appPassword.refresh();
|
||||
$scope.tokens.refresh();
|
||||
Client.refreshUserInfo(); // 2fa status might have changed by admin
|
||||
Client.refreshProfile(); // 2fa status might have changed by admin
|
||||
|
||||
$translate.onReady(function () {
|
||||
var usedLang = $translate.use() || $translate.fallbackLanguage();
|
||||
|
||||
@@ -12,11 +12,14 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" style="display: block;" for="memoryLimit">
|
||||
{{ 'services.memoryLimit' | tr }}: <b>{{ serviceConfigure.memoryLimit / 1024 / 1024 + 'MB' }}</b>
|
||||
{{ 'services.memoryLimit' | tr }}: <b>{{ serviceConfigure.memoryLimit | prettyBinarySize:'' }}</b>
|
||||
<button type="button" class="btn btn-xs btn-default pull-right" ng-click="serviceConfigure.resetToDefaults()">{{ 'services.configure.resetToDefaults' | tr }}</button>
|
||||
</label>
|
||||
<div style="padding: 0 10px;">
|
||||
<slider id="memoryLimit" ng-model="serviceConfigure.memoryLimit" step="134217728" tooltip="hide" ticks="serviceConfigure.memoryTicks" ticks-snap-bounds="67108864"></slider>
|
||||
<input type="range" id="memoryLimit" ng-model="serviceConfigure.memoryLimit" step="134217728" min="{{ serviceConfigure.memoryTicks[0] }}" max="{{ serviceConfigure.memoryTicks[serviceConfigure.memoryTicks.length-1] }}" list="memoryLimitTicks" />
|
||||
<datalist id="memoryLimitTicks">
|
||||
<option ng-repeat="limit in serviceConfigure.memoryTicks" value="{{ limit }}"></option>
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -112,9 +115,10 @@
|
||||
<span ng-show="service.config.memoryLimit">{{ service.config.memoryLimit | prettyBinarySize }}</span>
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-default" ng-click="serviceConfigure.show(service)" uib-tooltip="{{ 'services.configureActionTooltip' | tr }}" ng-disabled="!service.config.memoryLimit"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="serviceConfigure.show(service)" uib-tooltip="{{ 'services.configureActionTooltip' | tr }}" ng-disabled="service.status === 'disabled' || !service.config.memoryLimit"><i class="fa fa-pencil-alt"></i></button>
|
||||
<!-- restart is always clickable so that a user can rebuild mongodb in disabled state when using VMs where CPU flags can be dynamically changed -->
|
||||
<button class="btn btn-xs btn-default" ng-click="restartService(service.name)" uib-tooltip="{{ 'services.restartActionTooltip' | tr }}"><i class="fa fa-sync-alt" ng-class="{ 'fa-spin': service.status === 'starting' && !service.config.recoveryMode }"></i></button>
|
||||
<a class="btn btn-xs btn-default" ng-href="{{ '/frontend/logs.html?id=' + service.name }}" target="_blank" uib-tooltip="{{ 'logs.title' | tr }}"><i class="fa fa-file-alt"></i></a>
|
||||
<a class="btn btn-xs btn-default" ng-href="{{ service.status === 'disabled' ? '' : ('/frontend/logs.html?id=' + service.name) }}" target="_blank" uib-tooltip="{{ 'logs.title' | tr }}" ng-disabled="service.status === 'disabled'"><i class="fa fa-file-alt"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="hasRedisServices" ng-click="redisServicesExpanded = !redisServicesExpanded" class="hand">
|
||||
|
||||
@@ -20,12 +20,13 @@ angular.module('Application').controller('ServicesController', ['$scope', '$loca
|
||||
if (error) return console.log('Error getting status of ' + serviceName + ':' + error.message);
|
||||
|
||||
var service = $scope.services.find(function (s) { return s.name === serviceName; });
|
||||
if (!service) $scope.services[serviceName] = service;
|
||||
if (!service) callback(new Error('no such service' + serviceName)); // cannot happen
|
||||
|
||||
service.status = result.status;
|
||||
service.config = result.config;
|
||||
service.memoryUsed = result.memoryUsed;
|
||||
service.memoryPercent = result.memoryPercent;
|
||||
service.defaultMemoryLimit = result.defaultMemoryLimit;
|
||||
|
||||
callback(null, service);
|
||||
});
|
||||
@@ -53,7 +54,10 @@ angular.module('Application').controller('ServicesController', ['$scope', '$loca
|
||||
|
||||
return;
|
||||
}
|
||||
if (error) return Client.error(error);
|
||||
if (error) {
|
||||
refresh(serviceName);
|
||||
return Client.error(error);
|
||||
}
|
||||
|
||||
// show "busy" indicator for 3 seconds to show some ui activity
|
||||
setTimeout(function () { waitForActive(serviceName); }, 3000);
|
||||
@@ -75,19 +79,22 @@ angular.module('Application').controller('ServicesController', ['$scope', '$loca
|
||||
$scope.serviceConfigure.reset();
|
||||
|
||||
$scope.serviceConfigure.service = service;
|
||||
$scope.serviceConfigure.memoryLimit = service.config.memoryLimit;
|
||||
$scope.serviceConfigure.recoveryMode = !!service.config.recoveryMode;
|
||||
|
||||
$scope.serviceConfigure.memoryTicks = [];
|
||||
// we max system memory and current service memory for the case where the user configured the service on another server with more resources
|
||||
var nearest256m = Math.ceil(Math.max($scope.memory.memory, service.config.memoryLimit) / (256*1024*1024)) * 256 * 1024 * 1024;
|
||||
var startTick = service.defaultMemoryLimit;
|
||||
|
||||
// create ticks starting from manifest memory limit. the memory limit here is currently split into ram+swap (and thus *2 below)
|
||||
// TODO: the *2 will overallocate since 4GB is max swap that cloudron itself allocates
|
||||
$scope.serviceConfigure.memoryTicks = [];
|
||||
var npow2 = Math.pow(2, Math.ceil(Math.log($scope.memory.memory)/Math.log(2)));
|
||||
for (var i = 256; i <= (npow2*2/1024/1024); i *= 2) {
|
||||
$scope.serviceConfigure.memoryTicks.push(i * 1024 * 1024);
|
||||
for (var i = startTick; i <= nearest256m; i *= 2) {
|
||||
$scope.serviceConfigure.memoryTicks.push(i);
|
||||
}
|
||||
|
||||
// for firefox widget update
|
||||
$timeout(function() {
|
||||
$scope.serviceConfigure.memoryLimit = service.config.memoryLimit;
|
||||
}, 500);
|
||||
|
||||
$('#serviceConfigureModal').modal('show');
|
||||
},
|
||||
|
||||
@@ -96,7 +103,7 @@ angular.module('Application').controller('ServicesController', ['$scope', '$loca
|
||||
$scope.serviceConfigure.error = null;
|
||||
|
||||
var data = {
|
||||
memoryLimit: $scope.serviceConfigure.memoryLimit,
|
||||
memoryLimit: parseInt($scope.serviceConfigure.memoryLimit),
|
||||
recoveryMode: $scope.serviceConfigure.recoveryMode
|
||||
};
|
||||
|
||||
@@ -119,7 +126,7 @@ angular.module('Application').controller('ServicesController', ['$scope', '$loca
|
||||
},
|
||||
|
||||
resetToDefaults: function () {
|
||||
$scope.serviceConfigure.memoryLimit = 256 * 1024 * 1024; // 256MB default
|
||||
$scope.serviceConfigure.memoryLimit = $scope.serviceConfigure.service.defaultMemoryLimit;
|
||||
},
|
||||
|
||||
reset: function () {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<h4 class="modal-title">{{ 'settings.updateDialog.title' | tr }} <b>{{config.update.box.version}}</b> </h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-hide="installedApps | readyToUpdate">
|
||||
<div ng-hide="installedApps | canUpdate">
|
||||
<p>{{ 'settings.updateDialog.blockingApps' | tr }}</p>
|
||||
<ul>
|
||||
<li ng-repeat="app in installedApps | inProgressApps">{{app.fqdn}}</li>
|
||||
@@ -17,7 +17,7 @@
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<div ng-show="installedApps | readyToUpdate">
|
||||
<div ng-show="installedApps | canUpdate">
|
||||
<p class="text-danger" ng-show="config.update.box.unstable">{{ 'settings.updateDialog.unstableWarning' | tr }}</p>
|
||||
<p>{{ 'settings.updateDialog.changes' | tr }}:</p>
|
||||
<ul>
|
||||
@@ -28,12 +28,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<label class="checkbox-inline pull-left">
|
||||
<label ng-show="installedApps | canUpdate" class="checkbox-inline pull-left">
|
||||
<input type="checkbox" ng-model="update.skipBackup">{{ 'settings.updateDialog.skipBackupCheckbox' | tr }}
|
||||
</label>
|
||||
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn" ng-class="config.update.box.unstable ? 'btn-danger' : 'btn-success'" ng-click="update.startUpdate()" ng-disabled="update.busy" ng-show="(installedApps | readyToUpdate)"><i class="fa fa-circle-notch fa-spin" ng-show="update.busy"></i> {{ 'settings.updateDialog.updateAction' | tr }}</button>
|
||||
<button type="button" class="btn" ng-show="installedApps | canUpdate" ng-class="config.update.box.unstable ? 'btn-danger' : 'btn-success'" ng-click="update.startUpdate()" ng-disabled="update.busy"><i class="fa fa-circle-notch fa-spin" ng-show="update.busy"></i> {{ 'settings.updateDialog.updateAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,66 +5,33 @@
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'support.ticket.title' | tr }}</h3>
|
||||
<h3>{{ 'support.help.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="grid-item-top">
|
||||
<div class="row" ng-hide="ready">
|
||||
<h2 class="text-center"><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
<div class="row" ng-show="ready">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div ng-show="subscription && !subscription.emailVerified" style="margin-bottom: 30px;">
|
||||
<p class="text-bold">
|
||||
{{ 'support.ticket.emailNotVerified' | tr:{ email: subscription.email } }}
|
||||
<br/>
|
||||
<center>
|
||||
<a ng-href="{{ config.consoleServerOrigin }}" target="_blank" class="btn btn-success">{{ 'support.ticket.emailVerifyAction' | tr }}</a>
|
||||
</center>
|
||||
</p>
|
||||
<div ng-bind-html="'support.help.description' | tr:{ docsLink: 'https://docs.cloudron.io/?support_view', packagingLink: 'https://docs.cloudron.io/custom-apps/tutorial/?support_view', forumLink: 'https://forum.cloudron.io/' } | markdown2html"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left" ng-if="troubleshoot">
|
||||
<h3>Troubleshoot</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" ng-if="troubleshoot">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<p>Troubleshooting tools</p>
|
||||
<div>
|
||||
<button class="btn btn-default pull-right" ng-click="repairAll()"><i ng-show="repairAllBusy" class="fa fa-circle-notch fa-spin"></i> Repair All</button>
|
||||
<button class="btn btn-default pull-right" ng-click="updateAll()"><i ng-show="updateAllBusy" class="fa fa-circle-notch fa-spin"></i> Update All</button>
|
||||
</div>
|
||||
|
||||
<div ng-bind-html="supportConfig.ticketFormBody | markdown2html"></div>
|
||||
|
||||
<form ng-show="supportConfig.submitTickets" name="feedbackForm" ng-submit="submitFeedback()">
|
||||
<div class="form-group">
|
||||
<label>{{ 'support.ticket.type' | tr }}</label>
|
||||
<select class="form-control" name="type" style="width: 50%;" ng-model="feedback.type" required ng-disabled="!subscription.emailVerified">
|
||||
<option value="app_error">{{ 'support.ticket.typeApp' | tr }}</option>
|
||||
<option value="ticket">{{ 'support.ticket.typeBug' | tr }}</option>
|
||||
<option value="billing">{{ 'support.ticket.typeBilling' | tr }}</option>
|
||||
<option value="email_error">{{ 'support.ticket.typeEmail' | tr }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" ng-show="feedback.type === 'app_error'">
|
||||
<label>{{ 'support.ticket.selectApp' | tr }}</label>
|
||||
<select class="form-control" name="type" style="width: 50%;" ng-model="feedback.appId" ng-required="feedback.type === 'app_error'" ng-disabled="!subscription.emailVerified">
|
||||
<option ng-repeat="app in apps" value="{{ app.id }}">{{ app.fqdn }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (feedbackForm.subject.$dirty && feedbackForm.subject.$invalid) }">
|
||||
<label>{{ 'support.ticket.topic' | tr }}</label>
|
||||
<input type="text" class="form-control" name="subject" ng-model="feedback.subject" ng-maxlength="512" ng-minlength="1" required ng-disabled="!subscription.emailVerified">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (feedbackForm.description.$dirty && feedbackForm.description.$invalid) }">
|
||||
<label>{{ 'support.ticket.report' | tr }}</label>
|
||||
<textarea class="form-control" name="description" rows="3" placeholder="{{ 'support.ticket.reportPlaceholder' | tr }}" ng-model="feedback.description" ng-minlength="1" required ng-disabled="!subscription.emailVerified"></textarea>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': feedbackForm.email.$invalid }">
|
||||
<label>{{ 'support.ticket.email' | tr }}</label> <small>{{ 'support.ticket.emailInfo' | tr:{ email: subscription.email } }}</small>
|
||||
<input type="email" class="form-control" name="email" placeholder="{{ 'support.ticket.emailPlaceholder' | tr }}" ng-model="feedback.altEmail" ng-required="feedback.type === 'email_error'" ng-disabled="!subscription.emailVerified">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">
|
||||
<input type="checkbox" ng-model="feedback.enableSshSupport" ng-disabled="!subscription.emailVerified"> {{ 'support.ticket.sshCheckbox' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary pull-right" ng-disabled="!subscription.emailVerified || feedbackForm.$invalid || feedback.busy"><i class="fa fa-circle-notch fa-spin" ng-show="feedback.busy"></i> {{ 'support.ticket.submitAction' | tr }}</button>
|
||||
<span ng-show="feedback.error" class="text-danger text-bold">{{feedback.error}}</span>
|
||||
<span ng-show="feedback.result" class="text-success text-bold">{{feedback.result.message}}</span>
|
||||
</form>
|
||||
<p class="text-small text-warning">{{ troubleshootingMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
/* global angular:false */
|
||||
/* global $:false */
|
||||
/* global ISTATES */
|
||||
/* global async */
|
||||
|
||||
angular.module('Application').controller('SupportController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastOwner) $location.path('/'); });
|
||||
@@ -9,67 +11,59 @@ angular.module('Application').controller('SupportController', ['$scope', '$locat
|
||||
$scope.ready = false;
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.apps = Client.getInstalledApps();
|
||||
$scope.appsById = {};
|
||||
$scope.supportConfig = null;
|
||||
|
||||
$scope.feedback = {
|
||||
error: null,
|
||||
result: null,
|
||||
busy: false,
|
||||
enableSshSupport: false,
|
||||
subject: '',
|
||||
type: 'app_error',
|
||||
description: '',
|
||||
appId: '',
|
||||
altEmail: ''
|
||||
};
|
||||
$scope.installedApps = Client.getInstalledApps();
|
||||
|
||||
$scope.toggleSshSupportError = '';
|
||||
$scope.sshSupportEnabled = false;
|
||||
$scope.subscription = null;
|
||||
|
||||
function resetFeedback() {
|
||||
$scope.feedback.enableSshSupport = false;
|
||||
$scope.feedback.subject = '';
|
||||
$scope.feedback.description = '';
|
||||
$scope.feedback.type = 'app_error';
|
||||
$scope.feedback.appId = '';
|
||||
$scope.feedback.altEmail = '';
|
||||
$scope.troubleshoot = $location.search().troubleshoot;
|
||||
|
||||
$scope.feedbackForm.$setUntouched();
|
||||
$scope.feedbackForm.$setPristine();
|
||||
}
|
||||
$scope.updateAllBusy = false;
|
||||
$scope.repairAllBusy = false;
|
||||
$scope.troubleshootingMessage = '';
|
||||
|
||||
$scope.submitFeedback = function () {
|
||||
$scope.feedback.busy = true;
|
||||
$scope.feedback.result = null;
|
||||
$scope.feedback.error = null;
|
||||
$scope.updateAll = function () {
|
||||
$scope.updateAllBusy = true;
|
||||
$scope.troubleshootingMessage = '';
|
||||
let count = 0, unstable = 0;
|
||||
|
||||
var data = {
|
||||
enableSshSupport: $scope.feedback.enableSshSupport,
|
||||
subject: $scope.feedback.subject,
|
||||
description: $scope.feedback.description,
|
||||
type: $scope.feedback.type,
|
||||
appId: $scope.feedback.appId,
|
||||
altEmail: $scope.feedback.altEmail
|
||||
};
|
||||
Client.checkForUpdates(function (error) {
|
||||
if (error) Client.error(error);
|
||||
|
||||
Client.createTicket(data, function (error, result) {
|
||||
if (error) {
|
||||
$scope.feedback.error = error.message;
|
||||
} else {
|
||||
$scope.feedback.result = result;
|
||||
resetFeedback();
|
||||
}
|
||||
async.eachSeries(Object.keys($scope.config.update), function (appId, iteratorDone) {
|
||||
if ($scope.config.update[appId].unstable) { ++unstable; return iteratorDone(); }
|
||||
|
||||
$scope.feedback.busy = false;
|
||||
Client.updateApp(appId, $scope.config.update[appId].manifest, { skipBackup: false }, function (error) {
|
||||
if (error) Client.error(error);
|
||||
else ++count;
|
||||
|
||||
// refresh state
|
||||
Client.getRemoteSupport(function (error, enabled) {
|
||||
if (error) return console.error(error);
|
||||
iteratorDone();
|
||||
});
|
||||
}, function () {
|
||||
$scope.troubleshootingMessage = `${count} apps updated. ${unstable} apps with unstable updates skipped.`;
|
||||
$scope.updateAllBusy = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.sshSupportEnabled = enabled;
|
||||
$scope.repairAll = function () {
|
||||
$scope.repairAllBusy = true;
|
||||
$scope.troubleshootingMessage = '';
|
||||
let count = 0;
|
||||
|
||||
Client.refreshInstalledApps(function () {
|
||||
async.eachSeries($scope.installedApps, function (app, iteratorDone) {
|
||||
if (app.installationState !== ISTATES.ERROR) return iteratorDone();
|
||||
|
||||
Client.repairApp(app.id, {}, function (error) {
|
||||
if (error) Client.error(error);
|
||||
else ++count;
|
||||
|
||||
iteratorDone();
|
||||
});
|
||||
}, function () {
|
||||
$scope.troubleshootingMessage = `${count} apps repaired.`;
|
||||
$scope.repairAllBusy = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -89,27 +83,12 @@ angular.module('Application').controller('SupportController', ['$scope', '$locat
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
Client.getSubscription(function (error, result) {
|
||||
if (error && error.statusCode === 402) return $scope.ready = true; // not yet registered
|
||||
if (error && error.statusCode === 412) return $scope.ready = true; // invalid appstore token
|
||||
Client.getRemoteSupport(function (error, enabled) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.subscription = result;
|
||||
$scope.sshSupportEnabled = enabled;
|
||||
|
||||
Client.getSupportConfig(function (error, supportConfig) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.supportConfig = supportConfig;
|
||||
|
||||
Client.getRemoteSupport(function (error, enabled) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
Client.getInstalledApps().forEach(function (app) { $scope.appsById[app.id] = app; });
|
||||
|
||||
$scope.sshSupportEnabled = enabled;
|
||||
$scope.ready = true;
|
||||
});
|
||||
});
|
||||
$scope.ready = true;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -11,10 +11,47 @@
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-12">
|
||||
|
||||
<h3 class="graphs-toolbar">
|
||||
Graphs
|
||||
{{ 'system.info.title' | tr }}
|
||||
</h3>
|
||||
|
||||
<div class="card card-expand">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 text-muted">{{ 'system.info.platformVersion' | tr }}</div>
|
||||
<div class="col-xs-8 text-right">v{{ config.version }} ({{ config.ubuntuVersion }})</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-4 text-muted">{{ 'system.info.vendor' | tr }}</div>
|
||||
<div class="col-xs-8 text-right">{{ info.sysVendor }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-4 text-muted">{{ 'system.info.product' | tr }}</div>
|
||||
<div class="col-xs-8 text-right">{{ info.productName }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-4 text-muted">CPU</div>
|
||||
<div class="col-xs-8 text-right">{{ cpus.length + ' Core "' + cpus[0].model + '"' }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-4 text-muted">{{ 'system.info.memory' | tr }}</div>
|
||||
<div class="col-xs-8 text-right">{{ memory.memory | prettyDiskSize }} RAM <span ng-show="memory.swap">& {{ memory.swap | prettyDiskSize }} Swap</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-4 text-muted">{{ 'system.info.uptime' | tr }}</div>
|
||||
<div class="col-xs-8 text-right">{{ info.uptimeSecs }}</div>
|
||||
</div>
|
||||
<div class="row" ng-show="info.activationTime">
|
||||
<div class="col-xs-4 text-muted">{{ 'system.info.activationTime' | tr }}</div>
|
||||
<div class="col-xs-8 text-right">{{ info.activationTime | prettyDate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h3 class="graphs-toolbar">
|
||||
{{ 'system.graphs.title' | tr }}
|
||||
<div class="graphs-toolbar-actions">
|
||||
<button class="btn btn-sm btn-default" style="margin-right: 5px;" ng-click="graphs.refresh()" ng-disabled="graphs.busy"><i class="fas fa-sync-alt" ng-class="{ 'fa-spin': graphs.busy }"></i></button>
|
||||
<div class="dropdown">
|
||||
@@ -85,13 +122,13 @@
|
||||
<div ng-repeat="content in disk.contents" class="disk-content">
|
||||
<span class="color-indicator" style="background-color: {{ content.color }};"> </span>
|
||||
<span ng-show="content.type === 'cloudron-backup-default'">{{ content.path }} (Old Backups)</span>
|
||||
<span ng-show="content.type === 'standard'">{{ content.label || content.id }}</span>
|
||||
<span ng-show="content.type === 'swap'">{{ content.id }}</span>
|
||||
<span ng-show="content.type === 'standard'">{{ content.label }}</span>
|
||||
<span ng-show="content.type === 'swap'">{{ content.label }}</span>
|
||||
<span ng-show="content.type === 'app'">
|
||||
<a href="https://{{ content.app.fqdn }}" target="_blank" ng-hide="content.uninstalled">{{ content.app.label || content.app.fqdn }}</a>
|
||||
<a href="/#/app/{{ content.app.id }}/storage" ng-hide="content.uninstalled">{{ content.label }}</a>
|
||||
<span ng-show="content.uninstalled">{{ 'system.diskUsage.uninstalledApp' | tr }}</span>
|
||||
</span>
|
||||
<span ng-show="content.type === 'volume'"><a href="/#/volumes">{{ content.volume.name }}</a></span>
|
||||
<span ng-show="content.type === 'volume'"><a href="/#/volumes">{{ content.label }}</a></span>
|
||||
<small class="text-muted">{{ content.usage | prettyDiskSize }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
/* global angular */
|
||||
/* global $ */
|
||||
/* global TASK_TYPES */
|
||||
/* global Chart */
|
||||
|
||||
angular.module('Application').controller('SystemController', ['$scope', '$location', '$timeout', 'Client', function ($scope, $location, $timeout, Client) {
|
||||
@@ -9,6 +10,8 @@ angular.module('Application').controller('SystemController', ['$scope', '$locati
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.memory = null;
|
||||
$scope.cpus = null;
|
||||
$scope.info = null;
|
||||
$scope.volumesById = {};
|
||||
|
||||
// https://stackoverflow.com/questions/1484506/random-color-generator
|
||||
@@ -94,10 +97,15 @@ angular.module('Application').controller('SystemController', ['$scope', '$locati
|
||||
if (content.type === 'app') {
|
||||
content.app = Client.getInstalledAppsByAppId()[content.id];
|
||||
if (!content.app) content.uninstalled = true;
|
||||
else content.label = content.app.label || content.app.fqdn;
|
||||
} else if (content.type === 'volume') {
|
||||
content.volume = $scope.volumesById[content.id];
|
||||
content.label = content.volume ? content.volume.name : 'Removed volume';
|
||||
}
|
||||
|
||||
// ensure a label for ui
|
||||
content.label = content.label || content.id;
|
||||
|
||||
usageOther -= content.usage;
|
||||
});
|
||||
|
||||
@@ -321,6 +329,20 @@ angular.module('Application').controller('SystemController', ['$scope', '$locati
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
Client.cpus(function (error, cpus) {
|
||||
if (error) console.error(error);
|
||||
$scope.cpus = cpus;
|
||||
});
|
||||
|
||||
Client.systemInfo(function (error, info) {
|
||||
if (error) console.error(error);
|
||||
|
||||
// prettify for UI
|
||||
info.uptimeSecs = moment.duration(info.uptimeSecs, 'seconds').locale(navigator.language).humanize();
|
||||
|
||||
$scope.info = info;
|
||||
});
|
||||
|
||||
Client.memory(function (error, memory) {
|
||||
if (error) console.error(error);
|
||||
|
||||
|
||||
@@ -9,10 +9,14 @@
|
||||
<p class="has-error text-center" ng-show="externalLdap.error.generic">{{ externalLdap.error.generic }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="ldapProvider">{{ 'users.externalLdap.provider' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-management/#external-directory" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<label class="control-label" for="ldapProvider">{{ 'users.externalLdap.provider' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-directory/#external-directory" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" id="ldapProvider" ng-model="externalLdap.provider" ng-options="a.value as a.name for a in ldapProvider"></select>
|
||||
</div>
|
||||
|
||||
<p class="text-small text-warning" ng-show="externalLdap.provider === 'noop' && externalLdap.currentConfig.provider !== 'noop'">
|
||||
{{ 'users.externalLdap.disableWarning' | tr }}
|
||||
</p>
|
||||
|
||||
<div uib-collapse="externalLdap.provider === 'noop'">
|
||||
<form name="externalLdapConfigForm" role="form" novalidate ng-submit="externalLdap.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
@@ -93,7 +97,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="externalLdap.submit()" ng-disabled="externalLdapConfigForm.$invalid || externalLdap.busy"><i class="fa fa-circle-notch fa-spin" ng-show="externalLdap.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="externalLdap.submit()" ng-disabled="externalLdapConfigForm.$invalid || externalLdap.saveBusy"><i class="fa fa-circle-notch fa-spin" ng-show="externalLdap.saveBusy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -112,21 +116,11 @@
|
||||
<br/>
|
||||
<br/>
|
||||
<form name="clientAddForm" role="form" novalidate ng-submit="clientAdd.submit()" autocomplete="off">
|
||||
<p class="text-danger" ng-show="clientAdd.error">{{ clientAdd.error }}</p>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="clientName">{{ 'oidc.client.name' | tr }}</label>
|
||||
<input type="text" id="clientName" class="form-control" name="clientName" ng-model="clientAdd.name" autofocus required/>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': clientAdd.error.id }">
|
||||
<label class="control-label" for="clientId">{{ 'oidc.client.id' | tr }}</label>
|
||||
<input type="text" id="clientId" class="form-control" name="clientId" ng-model="clientAdd.id" required/>
|
||||
<div class="control-label" ng-show="clientAdd.error.id">
|
||||
<small>{{ clientAdd.error.id }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="clientSecret">{{ 'oidc.client.secret' | tr }}</label>
|
||||
<input type="text" id="clientSecret" class="form-control" name="clientSecret" ng-model="clientAdd.secret" required/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="loginRedirectUri">{{ 'oidc.client.loginRedirectUri' | tr }}</label>
|
||||
<input type="text" id="loginRedirectUri" class="form-control" name="loginRedirectUri" ng-model="clientAdd.loginRedirectUri" required/>
|
||||
@@ -158,18 +152,37 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'oidc.editClientDialog.title' | tr:{ client: clientEdit.id } }}</h4>
|
||||
<h4 class="modal-title">{{ 'oidc.editClientDialog.title' | tr:{ client: clientEdit.name } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="clientEditForm" role="form" novalidate ng-submit="clientEdit.submit()" autocomplete="off">
|
||||
<p class="text-danger" ng-show="clientEdit.error">{{ clientEdit.error }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'oidc.client.id' | tr }}</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="clientIdInput" class="form-control" ng-value="clientEdit.id" readonly/>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-primary" id="clientIdInputClipboardButton" type="button" data-clipboard-target="#clientIdInput"><i class="fa fa-clipboard"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'oidc.client.secret' | tr }}</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="clientSecretInput" class="form-control" ng-value="clientEdit.secret" readonly/>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-primary" id="clientSecretInputClipboardButton" type="button" data-clipboard-target="#clientSecretInput"><i class="fa fa-clipboard"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputEditClientName">{{ 'oidc.client.name' | tr }}</label>
|
||||
<input type="text" id="inputEditClientName" class="form-control" name="clientName" ng-model="clientEdit.name" autofocus required/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputEditClientSecret">{{ 'oidc.client.secret' | tr }}</label>
|
||||
<input type="text" id="inputEditClientSecret" class="form-control" name="clientSecret" ng-model="clientEdit.secret" required/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputEditLoginRedirectUri">{{ 'oidc.client.loginRedirectUri' | tr }}</label>
|
||||
<input type="text" id="inputEditLoginRedirectUri" class="form-control" name="loginRedirectUri" ng-model="clientEdit.loginRedirectUri" required/>
|
||||
@@ -228,7 +241,7 @@
|
||||
<fieldset ng-disabled="profileConfig.busy">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="profileConfig.editableUserProfiles"> {{ 'users.settings.allowProfileEditCheckbox' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-management/#lock-profile" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup>
|
||||
<input type="checkbox" ng-model="profileConfig.editableUserProfiles"> {{ 'users.settings.allowProfileEditCheckbox' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-directory/#lock-profile" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
@@ -254,7 +267,21 @@
|
||||
</div>
|
||||
|
||||
<div class="text-left section-header">
|
||||
<h3>{{ 'users.externalLdap.title' | tr }}</h3>
|
||||
<h3>
|
||||
{{ 'users.externalLdap.title' | tr }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="externalLdap.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'domains.renewCerts.showLogsAction' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in externalLdap.tasks">
|
||||
<a ng-href="/frontend/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large">
|
||||
@@ -264,150 +291,138 @@
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row" ng-hide="config.features.externalLdap">
|
||||
<div class="col-md-12">
|
||||
{{ 'users.externalLdap.subscriptionRequired' | tr }} <a href="" class="pull-right" ng-click="openSubscriptionSetup()">{{ 'users.externalLdap.subscriptionRequiredAction' | tr }}</a>
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider === 'noop'">
|
||||
<div class="col-xs-12">
|
||||
<span class="text-muted">{{ 'users.externalLdap.noopInfo' | tr }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="config.features.externalLdap">
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.provider' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.provider }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider === 'noop'">
|
||||
<div class="col-xs-12">
|
||||
<span class="text-muted">{{ 'users.externalLdap.noopInfo' | tr }}</span>
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.server' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.url }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.acceptSelfSignedCert' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.acceptSelfSignedCerts ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.baseDn' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.baseDn }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.filter' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.filter }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.usernameField' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.usernameField || 'uid' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.syncGroups' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.syncGroups ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron' && externalLdap.currentConfig.syncGroups">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.groupBaseDn' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.groupBaseDn }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron' && externalLdap.currentConfig.syncGroups">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.groupFilter' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.groupFilter }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron' && externalLdap.currentConfig.syncGroups">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.groupnameField' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.groupnameField }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.auth' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.bindDn ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.autocreateUsersOnLogin' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.autoCreate ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<br/>
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<div ng-show="externalLdap.busy" class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ externalLdap.percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.provider' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.provider }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.server' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.url }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.acceptSelfSignedCert' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.acceptSelfSignedCerts ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.baseDn' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.baseDn }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.filter' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.filter }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.usernameField' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.usernameField || 'uid' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.syncGroups' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.syncGroups ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron' && externalLdap.currentConfig.syncGroups">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.groupBaseDn' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.groupBaseDn }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron' && externalLdap.currentConfig.syncGroups">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.groupFilter' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.groupFilter }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron' && externalLdap.currentConfig.syncGroups">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.groupnameField' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.groupnameField }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.auth' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.bindDn ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'users.externalLdap.autocreateUsersOnLogin' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ externalLdap.currentConfig.autoCreate ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<br/>
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<div ng-show="externalLdap.syncBusy" class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ externalLdap.percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p ng-show="externalLdap.syncBusy">{{ externalLdap.message }}</p>
|
||||
<p ng-hide="externalLdap.syncBusy">
|
||||
<div class="has-error" ng-show="!externalLdap.active">{{ externalLdap.errorMessage }}</div>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 text-right">
|
||||
<button class="btn btn-primary pull-right" ng-click="externalLdap.show()">{{ 'users.externalLdap.configureAction' | tr }}</button>
|
||||
<button class="btn btn-success pull-right" ng-disabled="externalLdap.currentConfig.provider === 'noop'" ng-click="externalLdap.sync()"><i class="fa fa-circle-notch fa-spin" ng-show="externalLdap.syncBusy"></i> {{ 'users.externalLdap.syncAction' | tr }}</button>
|
||||
<a class="btn btn-primary pull-right" ng-show="externalLdap.taskId" ng-href="/frontend/logs.html?taskId={{ externalLdap.taskId }}" target="_blank">{{ 'users.externalLdap.showLogsAction' | tr }}</a>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-show="externalLdap.busy">{{ externalLdap.message }}</p>
|
||||
<p ng-hide="externalLdap.busy">
|
||||
<div class="has-error" ng-show="!externalLdap.active">{{ externalLdap.errorMessage }}</div>
|
||||
</p>
|
||||
<button class="btn btn-primary pull-right" ng-click="externalLdap.show()">{{ 'users.externalLdap.configureAction' | tr }}</button>
|
||||
<button class="btn btn-success pull-right" ng-disabled="externalLdap.currentConfig.provider === 'noop'" ng-click="externalLdap.sync()"><i class="fa fa-circle-notch fa-spin" ng-show="externalLdap.syncBusy"></i> {{ 'users.externalLdap.syncAction' | tr }}</button>
|
||||
<a class="btn btn-primary pull-right" ng-show="externalLdap.taskId" ng-href="/frontend/logs.html?taskId={{ externalLdap.taskId }}" target="_blank">{{ 'users.externalLdap.showLogsAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -427,7 +442,7 @@
|
||||
<fieldset>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="userDirectoryConfig.enabled" ng-disabled="userDirectoryConfig.busy"> {{ 'users.exposedLdap.enabled' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-management/#directory-server" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup>
|
||||
<input type="checkbox" ng-model="userDirectoryConfig.enabled" ng-disabled="userDirectoryConfig.busy"> {{ 'users.exposedLdap.enabled' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-directory/#directory-server" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -438,6 +453,7 @@
|
||||
<button class="btn btn-default" type="button" id="userDirectoryUrlClipboardButton" data-clipboard-target="#userDirectoryUrlInput"><i class="fa fa-clipboard"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-small text-warning text-bold" ng-show="adminDomain.provider === 'cloudflare'">{{ 'users.exposedLdap.cloudflarePortWarning' | tr }} </p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'users.exposedLdap.secret.label' | tr }}</label>
|
||||
@@ -447,7 +463,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'users.exposedLdap.ipRestriction.label' | tr }}</label>
|
||||
<p class="small">{{ 'users.exposedLdap.ipRestriction.description' | tr }}</p>
|
||||
<p class="small" ng-bind-html=" 'users.exposedLdap.ipRestriction.description' | tr "></p>
|
||||
<textarea ng-model="userDirectoryConfig.allowlist" ng-disabled="!userDirectoryConfig.enabled || userDirectoryConfig.busy" placeholder="{{ 'users.exposedLdap.ipRestriction.placeholder' | tr }}" name="allowlist" class="form-control" ng-class="{ 'has-error': !userDirectoryConfigForm.allowlist.$dirty && userDirectoryConfig.error.allowlist }" rows="4"></textarea>
|
||||
<div class="has-error" ng-show="userDirectoryConfig.error.allowlist">{{ userDirectoryConfig.error.allowlist }}</div>
|
||||
</div>
|
||||
@@ -477,7 +493,7 @@
|
||||
<div class="col-md-12">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">{{ 'oidc.env.discoveryUrl' | tr }}</td>
|
||||
<td class="text-muted" style="vertical-align: top;">{{ 'oidc.env.discoveryUrl' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-directory/#endpoints" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></td>
|
||||
<td class="text-right" style="vertical-align: top;" ng-click-select>https://{{ config.adminFqdn }}/.well-known/openid-configuration</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -495,10 +511,8 @@
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 33%">{{ 'oidc.client.name' | tr }}</th>
|
||||
<th style="width: 33%">{{ 'oidc.client.id' | tr }}</th>
|
||||
<th style="width: 33%">{{ 'oidc.client.signingAlgorithm' | tr }}</th>
|
||||
<th style="width: 10%" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
<th style="width: 80%">{{ 'oidc.client.name' | tr }}</th>
|
||||
<th style="width: 20%" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -509,12 +523,6 @@
|
||||
<td class="text-left elide-table-cell hand" ng-click="clientEdit.show(client)">
|
||||
{{ client.name }}
|
||||
</td>
|
||||
<td class="text-left elide-table-cell hand" ng-click="clientEdit.show(client)">
|
||||
{{ client.id }}
|
||||
</td>
|
||||
<td class="text-left elide-table-cell hand" ng-click="clientEdit.show(client)">
|
||||
{{ client.tokenSignatureAlgorithm }}
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-danger" ng-click="deleteClient.show(client)" uib-tooltip="Delete"><i class="far fa-trash-alt"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="clientEdit.show(client)" uib-tooltip="Edit"><i class="fa fa-pencil-alt"></i></button>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
/* global angular */
|
||||
/* global Clipboard */
|
||||
/* global $ */
|
||||
/* global $, TASK_TYPES */
|
||||
|
||||
angular.module('Application').controller('UserSettingsController', ['$scope', '$location', '$translate', '$timeout', 'Client', function ($scope, $location, $translate, $timeout, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
@@ -25,6 +25,7 @@ angular.module('Application').controller('UserSettingsController', ['$scope', '$
|
||||
$scope.ready = false;
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.userInfo = Client.getUserInfo();
|
||||
$scope.adminDomain = null;
|
||||
$scope.oidcClients = [];
|
||||
|
||||
$scope.profileConfig = {
|
||||
@@ -42,9 +43,6 @@ angular.module('Application').controller('UserSettingsController', ['$scope', '$
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
// prevent the current user from getting locked out
|
||||
if ($scope.profileConfig.mandatory2FA && !$scope.userInfo.twoFactorAuthenticationEnabled) return Client.notify('', $translate.instant('users.settings.require2FAWarning'), true, 'error', '#/profile');
|
||||
|
||||
$scope.profileConfig.error = '';
|
||||
$scope.profileConfig.busy = true;
|
||||
$scope.profileConfig.success = false;
|
||||
@@ -66,6 +64,12 @@ angular.module('Application').controller('UserSettingsController', ['$scope', '$
|
||||
|
||||
$timeout(function () {
|
||||
$scope.profileConfig.busy = false;
|
||||
|
||||
// prevent the current user from getting locked out. if user ignores this, they have to use cloudron-support --admin-login
|
||||
if ($scope.profileConfig.mandatory2FA && !$scope.userInfo.twoFactorAuthenticationEnabled) {
|
||||
if ($scope.userInfo.source && $scope.config.external2FA) return; // no need for warning if 2fa is external
|
||||
Client.notify('', $translate.instant('users.settings.require2FAWarning'), true, 'error', '#/profile');
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
@@ -119,15 +123,15 @@ angular.module('Application').controller('UserSettingsController', ['$scope', '$
|
||||
busy: false,
|
||||
percent: 0,
|
||||
message: '',
|
||||
errorMessage: '',
|
||||
error: {},
|
||||
taskId: 0,
|
||||
errorMessage: '', // last task error
|
||||
tasks: [],
|
||||
|
||||
syncBusy: false,
|
||||
error: {}, // save error
|
||||
saveBusy: false,
|
||||
|
||||
// fields
|
||||
provider: 'noop',
|
||||
autoCreate: false,
|
||||
autoCreate: true,
|
||||
url: '',
|
||||
acceptSelfSignedCerts: false,
|
||||
baseDn: '',
|
||||
@@ -139,57 +143,43 @@ angular.module('Application').controller('UserSettingsController', ['$scope', '$
|
||||
|
||||
currentConfig: {},
|
||||
|
||||
checkStatus: function () {
|
||||
Client.getLatestTaskByType('syncExternalLdap', function (error, task) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!task) return;
|
||||
|
||||
$scope.externalLdap.taskId = task.id;
|
||||
$scope.externalLdap.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
sync: function () {
|
||||
$scope.externalLdap.syncBusy = true;
|
||||
|
||||
Client.startExternalLdapSync(function (error, taskId) {
|
||||
if (error) {
|
||||
$scope.externalLdap.syncBusy = false;
|
||||
console.error('Unable to start ldap syncer task.', error);
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.externalLdap.taskId = taskId;
|
||||
$scope.externalLdap.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function() {
|
||||
init: function () {
|
||||
Client.getExternalLdapConfig(function (error, result) {
|
||||
if (error) return console.error('Unable to get external ldap config.', error);
|
||||
|
||||
$scope.externalLdap.currentConfig = result;
|
||||
$scope.externalLdap.checkStatus();
|
||||
$scope.externalLdap.refreshTasks();
|
||||
});
|
||||
},
|
||||
|
||||
refreshTasks: function () {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_SYNC_EXTERNAL_LDAP, function (error, tasks) {
|
||||
if (error) return console.error(error);
|
||||
$scope.externalLdap.tasks = tasks.slice(0, 10);
|
||||
if ($scope.externalLdap.tasks.length && $scope.externalLdap.tasks[0].active) $scope.externalLdap.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
Client.getTask($scope.externalLdap.taskId, function (error, data) {
|
||||
var taskId = $scope.externalLdap.tasks[0].id;
|
||||
|
||||
Client.getTask(taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.externalLdap.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
$scope.externalLdap.syncBusy = false;
|
||||
$scope.externalLdap.busy = false;
|
||||
$scope.externalLdap.message = '';
|
||||
$scope.externalLdap.percent = 100; // indicates that 'result' is valid
|
||||
$scope.externalLdap.errorMessage = data.success ? '' : data.error.message;
|
||||
|
||||
$scope.externalLdap.refreshTasks(); // update the tasks list dropdown
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.externalLdap.syncBusy = true;
|
||||
$scope.externalLdap.busy = true;
|
||||
$scope.externalLdap.percent = data.percent;
|
||||
$scope.externalLdap.message = data.message;
|
||||
window.setTimeout($scope.externalLdap.updateStatus, 3000);
|
||||
window.setTimeout($scope.externalLdap.updateStatus, 500);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -214,8 +204,25 @@ angular.module('Application').controller('UserSettingsController', ['$scope', '$
|
||||
$('#externalLdapModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
sync: function () {
|
||||
$scope.externalLdap.busy = true;
|
||||
$scope.externalLdap.percent = 0;
|
||||
$scope.externalLdap.message = '';
|
||||
$scope.externalLdap.errorMessage = '';
|
||||
|
||||
Client.startExternalLdapSync(function (error) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
$scope.externalLdap.errorMessage = error.message;
|
||||
$scope.externalLdap.busy = false;
|
||||
} else {
|
||||
$scope.externalLdap.refreshTasks();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.externalLdap.saveBusy = true;
|
||||
$scope.externalLdap.error = {};
|
||||
|
||||
var config = {
|
||||
@@ -256,12 +263,13 @@ angular.module('Application').controller('UserSettingsController', ['$scope', '$
|
||||
}
|
||||
|
||||
Client.setExternalLdapConfig(config, function (error) {
|
||||
$scope.externalLdap.busy = false;
|
||||
$scope.externalLdap.saveBusy = false;
|
||||
|
||||
if (error) {
|
||||
if (error.statusCode === 424) {
|
||||
if (error.code === 'SELF_SIGNED_CERT_IN_CHAIN') $scope.externalLdap.error.acceptSelfSignedCerts = true;
|
||||
else $scope.externalLdap.error.url = true;
|
||||
$scope.externalLdap.error.generic = error.message;
|
||||
} else if (error.statusCode === 400 && error.message === 'invalid baseDn') {
|
||||
$scope.externalLdap.error.baseDn = true;
|
||||
} else if (error.statusCode === 400 && error.message === 'invalid filter') {
|
||||
@@ -282,7 +290,7 @@ angular.module('Application').controller('UserSettingsController', ['$scope', '$
|
||||
}
|
||||
} else {
|
||||
$('#externalLdapModal').modal('hide');
|
||||
$scope.externalLdap.refresh();
|
||||
$scope.externalLdap.init();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -298,16 +306,12 @@ angular.module('Application').controller('UserSettingsController', ['$scope', '$
|
||||
|
||||
$scope.clientAdd = {
|
||||
busy: false,
|
||||
error: {},
|
||||
id: '',
|
||||
error: null,
|
||||
name: '',
|
||||
secret: '',
|
||||
loginRedirectUri: '',
|
||||
tokenSignatureAlgorithm: '',
|
||||
|
||||
show: function () {
|
||||
$scope.clientAdd.id = '';
|
||||
$scope.clientAdd.secret = '';
|
||||
$scope.clientAdd.name = '';
|
||||
$scope.clientAdd.loginRedirectUri = '';
|
||||
$scope.clientAdd.tokenSignatureAlgorithm = 'RS256';
|
||||
@@ -320,19 +324,13 @@ angular.module('Application').controller('UserSettingsController', ['$scope', '$
|
||||
|
||||
submit: function () {
|
||||
$scope.clientAdd.busy = true;
|
||||
$scope.clientAdd.error = {};
|
||||
$scope.clientAdd.error = null;
|
||||
|
||||
Client.addOidcClient($scope.clientAdd.id, $scope.clientAdd.name, $scope.clientAdd.secret, $scope.clientAdd.loginRedirectUri, $scope.clientAdd.tokenSignatureAlgorithm, function (error) {
|
||||
Client.addOidcClient($scope.clientAdd.name, $scope.clientAdd.loginRedirectUri, $scope.clientAdd.tokenSignatureAlgorithm, function (error) {
|
||||
if (error) {
|
||||
if (error.statusCode === 409) {
|
||||
$scope.clientAdd.error.id = 'Client ID already exists';
|
||||
$('#clientId').focus();
|
||||
} else {
|
||||
console.error('Unable to add openid client.', error);
|
||||
}
|
||||
|
||||
$scope.clientAdd.error = error.message;
|
||||
console.error('Unable to add openid client.', error);
|
||||
$scope.clientAdd.busy = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -346,17 +344,17 @@ angular.module('Application').controller('UserSettingsController', ['$scope', '$
|
||||
|
||||
$scope.clientEdit = {
|
||||
busy: false,
|
||||
error: {},
|
||||
error: null,
|
||||
id: '',
|
||||
name: '',
|
||||
secret: '',
|
||||
name: '',
|
||||
loginRedirectUri: '',
|
||||
tokenSignatureAlgorithm: '',
|
||||
|
||||
show: function (client) {
|
||||
$scope.clientEdit.id = client.id;
|
||||
$scope.clientEdit.name = client.name;
|
||||
$scope.clientEdit.secret = client.secret;
|
||||
$scope.clientEdit.name = client.name;
|
||||
$scope.clientEdit.loginRedirectUri = client.loginRedirectUri;
|
||||
$scope.clientEdit.tokenSignatureAlgorithm = client.tokenSignatureAlgorithm;
|
||||
$scope.clientEdit.busy = false;
|
||||
@@ -368,14 +366,13 @@ angular.module('Application').controller('UserSettingsController', ['$scope', '$
|
||||
|
||||
submit: function () {
|
||||
$scope.clientEdit.busy = true;
|
||||
$scope.clientEdit.error = {};
|
||||
$scope.clientEdit.error = null;
|
||||
|
||||
Client.updateOidcClient($scope.clientEdit.id, $scope.clientEdit.name, $scope.clientEdit.secret, $scope.clientEdit.loginRedirectUri, $scope.clientEdit.tokenSignatureAlgorithm, function (error) {
|
||||
Client.updateOidcClient($scope.clientEdit.id, $scope.clientEdit.name, $scope.clientEdit.loginRedirectUri, $scope.clientEdit.tokenSignatureAlgorithm, function (error) {
|
||||
if (error) {
|
||||
$scope.clientEdit.error = error.message;
|
||||
console.error('Unable to edit openid client.', error);
|
||||
|
||||
$scope.clientEdit.busy = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -413,10 +410,15 @@ angular.module('Application').controller('UserSettingsController', ['$scope', '$
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
$scope.externalLdap.refresh();
|
||||
$scope.externalLdap.init();
|
||||
$scope.profileConfig.refresh();
|
||||
$scope.userDirectoryConfig.refresh();
|
||||
$scope.refreshOIDCClients();
|
||||
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) return console.error('Unable to list domains.', error);
|
||||
$scope.adminDomain = result.filter(function (d) { return d.domain === $scope.config.adminDomain; })[0];
|
||||
});
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
@@ -444,5 +446,41 @@ angular.module('Application').controller('UserSettingsController', ['$scope', '$
|
||||
$timeout(function () { $('#userDirectoryUrlClipboardButton').tooltip('hide'); }, 2000);
|
||||
});
|
||||
|
||||
new Clipboard('#clientIdInputClipboardButton').on('success', function(e) {
|
||||
$('#clientIdInputClipboardButton').tooltip({
|
||||
title: 'Copied!',
|
||||
trigger: 'manual'
|
||||
}).tooltip('show');
|
||||
|
||||
$timeout(function () { $('#clientIdInputClipboardButton').tooltip('hide'); }, 2000);
|
||||
|
||||
e.clearSelection();
|
||||
}).on('error', function(/*e*/) {
|
||||
$('#clientIdInputClipboardButton').tooltip({
|
||||
title: 'Press Ctrl+C to copy',
|
||||
trigger: 'manual'
|
||||
}).tooltip('show');
|
||||
|
||||
$timeout(function () { $('#clientIdInputClipboardButton').tooltip('hide'); }, 2000);
|
||||
});
|
||||
|
||||
new Clipboard('#clientSecretInputClipboardButton').on('success', function(e) {
|
||||
$('#clientSecretInputClipboardButton').tooltip({
|
||||
title: 'Copied!',
|
||||
trigger: 'manual'
|
||||
}).tooltip('show');
|
||||
|
||||
$timeout(function () { $('#clientSecretInputClipboardButton').tooltip('hide'); }, 2000);
|
||||
|
||||
e.clearSelection();
|
||||
}).on('error', function(/*e*/) {
|
||||
$('#clientSecretInputClipboardButton').tooltip({
|
||||
title: 'Press Ctrl+C to copy',
|
||||
trigger: 'manual'
|
||||
}).tooltip('show');
|
||||
|
||||
$timeout(function () { $('#clientSecretInputClipboardButton').tooltip('hide'); }, 2000);
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
|
||||
@@ -1,22 +1,3 @@
|
||||
<!-- Modal make user local -->
|
||||
<div class="modal fade" id="makeLocalModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'users.makeLocalDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ 'users.makeLocalDialog.description' | tr }}</p>
|
||||
<p class="text-warning">{{ 'users.makeLocalDialog.warning' | tr }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="makeLocal.submit()" ng-disabled="makeLocal.busy"><i class="fa fa-circle-notch fa-spin" ng-show="makeLocal.busy"></i> {{ 'users.makeLocalDialog.submitAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal add user -->
|
||||
<div class="modal fade" id="userAddModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
@@ -25,49 +6,49 @@
|
||||
<h4 class="modal-title">{{ 'users.addUserDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="useraddForm" role="form" ng-submit="useradd.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (useraddForm.displayName.$dirty && useraddForm.displayName.$invalid) || (!useraddForm.displayName.$dirty && useradd.error.displayName) }">
|
||||
<form name="useraddForm" role="form" ng-submit="userAdd.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (useraddForm.displayName.$dirty && useraddForm.displayName.$invalid) || (!useraddForm.displayName.$dirty && userAdd.error.displayName) }">
|
||||
<label class="control-label">{{ 'users.user.fullName' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="useradd.displayName" name="displayName" id="inputUserAddDisplayName" autofocus autocomplete="off" placeholder="{{ 'users.user.displayNamePlaceholder' | tr }}">
|
||||
<div class="control-label" ng-show="(!useraddForm.displayName.$dirty && useradd.error.displayName) || (useraddForm.displayName.$dirty && useraddForm.displayName.$invalid) || (!useraddForm.displayName.$dirty && useradd.error.displayName)">
|
||||
<input type="text" class="form-control" ng-model="userAdd.displayName" name="displayName" id="inputUserAddDisplayName" autofocus autocomplete="off" placeholder="{{ 'users.user.displayNamePlaceholder' | tr }}">
|
||||
<div class="control-label" ng-show="(!useraddForm.displayName.$dirty && userAdd.error.displayName) || (useraddForm.displayName.$dirty && useraddForm.displayName.$invalid) || (!useraddForm.displayName.$dirty && userAdd.error.displayName)">
|
||||
<small ng-show="useraddForm.displayName.$error.displayName">{{ 'users.user.errorNotValidFullName' | tr }}</small>
|
||||
<small ng-show="!useraddForm.displayName.$dirty && useradd.error.displayName">{{ useradd.error.displayName }}</small>
|
||||
<small ng-show="!useraddForm.displayName.$dirty && userAdd.error.displayName">{{ userAdd.error.displayName }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (useraddForm.email.$dirty && useraddForm.email.$invalid) || (!useraddForm.email.$dirty && useradd.error.email) }">
|
||||
<div class="form-group" ng-class="{ 'has-error': (useraddForm.email.$dirty && useraddForm.email.$invalid) || (!useraddForm.email.$dirty && userAdd.error.email) }">
|
||||
<label class="control-label">{{ 'users.user.primaryEmail' | tr }} <sup><a ng-href="https://docs.cloudron.io/profile/#primary-email" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></label>
|
||||
<input type="email" class="form-control" ng-model="useradd.email" name="email" id="inputUserAddEmail" required>
|
||||
<div class="control-label" ng-show="(!useraddForm.email.$dirty && useradd.error.email) || (useraddForm.email.$dirty && useraddForm.email.$invalid) || (!useraddForm.email.$dirty && useradd.error.email)">
|
||||
<input type="email" class="form-control" ng-model="userAdd.email" name="email" id="inputUserAddEmail" required>
|
||||
<div class="control-label" ng-show="(!useraddForm.email.$dirty && userAdd.error.email) || (useraddForm.email.$dirty && useraddForm.email.$invalid) || (!useraddForm.email.$dirty && userAdd.error.email)">
|
||||
<small ng-show="useraddForm.email.$error.required">{{ 'users.user.errorEmailRequired' | tr }}</small>
|
||||
<small ng-show="useraddForm.email.$error.email">{{ 'users.user.errorInvalidEmail' | tr }}</small>
|
||||
<small ng-show="!useraddForm.email.$dirty && useradd.error.email">{{ useradd.error.email }}</small>
|
||||
<small ng-show="!useraddForm.email.$dirty && userAdd.error.email">{{ userAdd.error.email }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (useraddForm.fallbackEmail.$dirty && useraddForm.fallbackEmail.$invalid) || (!useraddForm.fallbackEmail.$dirty && useradd.error.fallbackEmail) }">
|
||||
<div class="form-group" ng-class="{ 'has-error': (useraddForm.fallbackEmail.$dirty && useraddForm.fallbackEmail.$invalid) || (!useraddForm.fallbackEmail.$dirty && userAdd.error.fallbackEmail) }">
|
||||
<label class="control-label">{{ 'users.user.recoveryEmail' | tr }} <sup><a ng-href="https://docs.cloudron.io/profile/#password-recovery-email" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></label>
|
||||
<input type="email" class="form-control" ng-model="useradd.fallbackEmail" name="fallbackEmail" id="inputUserAddFallbackEmail" placeholder="{{ 'users.user.fallbackEmailPlaceholder' | tr }}">
|
||||
<div class="control-label" ng-show="(!useraddForm.fallbackEmail.$dirty && useradd.error.fallbackEmail) || (useraddForm.fallbackEmail.$dirty && useraddForm.fallbackEmail.$invalid) || (!useraddForm.fallbackEmail.$dirty && useradd.error.fallbackEmail)">
|
||||
<input type="email" class="form-control" ng-model="userAdd.fallbackEmail" name="fallbackEmail" id="inputUserAddFallbackEmail" placeholder="{{ 'users.user.fallbackEmailPlaceholder' | tr }}">
|
||||
<div class="control-label" ng-show="(!useraddForm.fallbackEmail.$dirty && userAdd.error.fallbackEmail) || (useraddForm.fallbackEmail.$dirty && useraddForm.fallbackEmail.$invalid) || (!useraddForm.fallbackEmail.$dirty && userAdd.error.fallbackEmail)">
|
||||
<small ng-show="useraddForm.fallbackEmail.$error.required">{{ 'users.user.errorEmailRequired' | tr }}</small>
|
||||
<small ng-show="useraddForm.fallbackEmail.$error.fallbackEmail">{{ 'users.user.errorInvalidEmail' | tr }}</small>
|
||||
<small ng-show="!useraddForm.fallbackEmail.$dirty && useradd.error.fallbackEmail">{{ useradd.error.fallbackEmail }}</small>
|
||||
<small ng-show="!useraddForm.fallbackEmail.$dirty && userAdd.error.fallbackEmail">{{ userAdd.error.fallbackEmail }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (useraddForm.username.$dirty && useraddForm.username.$invalid) || (!useraddForm.username.$dirty && useradd.error.username) }">
|
||||
<div class="form-group" ng-class="{ 'has-error': (useraddForm.username.$dirty && useraddForm.username.$invalid) || (!useraddForm.username.$dirty && userAdd.error.username) }">
|
||||
<label class="control-label">{{ 'users.user.username' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="useradd.username" name="username" id="inputUserAddUsername" ng-required="config.profileLocked" placeholder="{{ config.profileLocked ? '' : ('users.user.usernamePlaceholder' | tr) }}">
|
||||
<div class="control-label" ng-show="(!useraddForm.username.$dirty && useradd.error.username) || (useraddForm.username.$dirty && useraddForm.username.$invalid) || (!useraddForm.username.$dirty && useradd.error.username)">
|
||||
<input type="text" class="form-control" ng-model="userAdd.username" name="username" id="inputUserAddUsername" ng-required="config.profileLocked" placeholder="{{ config.profileLocked ? '' : ('users.user.usernamePlaceholder' | tr) }}">
|
||||
<div class="control-label" ng-show="(!useraddForm.username.$dirty && userAdd.error.username) || (useraddForm.username.$dirty && useraddForm.username.$invalid) || (!useraddForm.username.$dirty && userAdd.error.username)">
|
||||
<small ng-show="useraddForm.username.$error.username">{{ 'users.user.errorInvalidUsername' | tr }}</small>
|
||||
<small ng-show="!useraddForm.username.$dirty && useradd.error.username">{{ useradd.error.username }}</small>
|
||||
<small ng-show="!useraddForm.username.$dirty && userAdd.error.username">{{ userAdd.error.username }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="userInfo.isAtLeastAdmin">
|
||||
<label class="control-label">{{ 'users.user.role' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-management/#roles" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<div class="control-label">
|
||||
<select class="form-control" ng-model="useradd.role" ng-options="role.id as role.name disable when role.disabled for role in roles"></select>
|
||||
<select class="form-control" ng-model="userAdd.role" ng-options="role.id as role.name disable when role.disabled for role in roles"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -75,22 +56,23 @@
|
||||
<label class="control-label">{{ 'users.user.groups' | tr }}</label>
|
||||
<div class="control-label">
|
||||
<div ng-show="groups.length === 0">{{ 'users.user.noGroups' | tr }}</div>
|
||||
<multiselect ng-show="groups.length !== 0" ng-model="useradd.selectedGroups" options="group.name for group in groups" data-compare-by="name" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<!-- local groups. they can have local and external users . angular cannot filter empty strings - https://github.com/angular/angular.js/issues/7890 -->
|
||||
<multiselect ng-show="groups.length !== 0" ng-model="userAdd.selectedLocalGroups" options="group.name for group in groups | filter:{ source: '!ldap' }" data-compare-by="name" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="useradd.sendInvite" id="inputUserAddSendInvite"> {{ 'users.addUserDialog.sendInviteCheckbox' | tr }}
|
||||
<input type="checkbox" ng-model="userAdd.sendInvite" id="inputUserAddSendInvite"> {{ 'users.addUserDialog.sendInviteCheckbox' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="useraddForm.$invalid || useradd.busy"/>
|
||||
<input class="ng-hide" type="submit" ng-disabled="useraddForm.$invalid || userAdd.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="useradd.submit()" ng-disabled="useraddForm.$invalid || useradd.busy"><i class="fa fa-circle-notch fa-spin" ng-show="useradd.busy"></i> {{ 'users.addUserDialog.addUserAction' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="userAdd.submit()" ng-disabled="useraddForm.$invalid || userAdd.busy"><i class="fa fa-circle-notch fa-spin" ng-show="userAdd.busy"></i> {{ 'users.addUserDialog.addUserAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,15 +83,15 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'users.deleteUserDialog.title' | tr:{ username: (userremove.userInfo.username || userremove.userInfo.email) } }}</h4>
|
||||
<h4 class="modal-title">{{ 'users.deleteUserDialog.title' | tr:{ username: (userRemove.userInfo.username || userRemove.userInfo.email) } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-bold text-danger" ng-show="userremove.error">{{ userremove.error }}</p>
|
||||
<p ng-hide="userremove.error">{{ 'users.deleteUserDialog.description' | tr }}</p>
|
||||
<p class="text-bold text-danger" ng-show="userRemove.error">{{ userRemove.error }}</p>
|
||||
<p ng-hide="userRemove.error">{{ 'users.deleteUserDialog.description' | tr }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="userremove.submit()" ng-hide="userremove.error" ng-disabled="userremove.busy"><i class="fa fa-circle-notch fa-spin" ng-show="userremove.busy"></i> {{ 'users.deleteUserDialog.deleteAction' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="userRemove.submit()" ng-hide="userRemove.error" ng-disabled="userRemove.busy"><i class="fa fa-circle-notch fa-spin" ng-show="userRemove.busy"></i> {{ 'users.deleteUserDialog.deleteAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -120,83 +102,94 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'users.editUserDialog.title' | tr:{ username: (useredit.userInfo.username || useredit.userInfo.email) } }}</h4>
|
||||
<h4 class="modal-title">{{ 'users.editUserDialog.title' | tr:{ username: (userEdit.userInfo.username || userEdit.userInfo.email) } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-show="useredit.source">
|
||||
<div ng-show="userEdit.source">
|
||||
<p class="text-warning">{{ 'users.editUserDialog.externalLdapWarning' | tr }}</p>
|
||||
<p><label>{{ 'users.user.displayName' | tr }}</label><br/><input type="text" class="form-control" ng-disabled="true" ng-model="useredit.displayName">
|
||||
<p><label>{{ 'users.user.email' | tr }}</label><br/><input type="text" class="form-control" ng-disabled="true" ng-model="useredit.email"></p>
|
||||
<p><label>{{ 'users.user.displayName' | tr }}</label><br/><input type="text" class="form-control" ng-disabled="true" ng-model="userEdit.displayName">
|
||||
<p><label>{{ 'users.user.email' | tr }}</label><br/><input type="text" class="form-control" ng-disabled="true" ng-model="userEdit.email"></p>
|
||||
</div>
|
||||
|
||||
<form name="useredit_form" role="form" ng-submit="useredit.submit()" autocomplete="off">
|
||||
<p class="has-error text-center" ng-show="useredit.error.generic">{{ useredit.error.generic }}</p>
|
||||
<form name="useredit_form" role="form" ng-submit="userEdit.submit()" autocomplete="off">
|
||||
<p class="has-error text-center" ng-show="userEdit.error.generic">{{ userEdit.error.generic }}</p>
|
||||
|
||||
<!-- when user profiles are locked, this provides a way for the admin to set the username -->
|
||||
<div class="form-group" ng-hide="useredit.source || useredit.userInfo.username" ng-class="{ 'has-error': (useredit_form.username.$dirty && useredit_form.username.$invalid) || (!useredit_form.username.$dirty && useredit.error.username) }">
|
||||
<div class="form-group" ng-hide="userEdit.source || userEdit.userInfo.username" ng-class="{ 'has-error': (useredit_form.username.$dirty && useredit_form.username.$invalid) || (!useredit_form.username.$dirty && userEdit.error.username) }">
|
||||
<label class="control-label">{{ 'users.user.username' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!useredit_form.username.$dirty && useredit.error.username) || (useredit_form.username.$dirty && useredit_form.username.$invalid) || (!useredit_form.username.$dirty && useredit.error.username)">
|
||||
<small ng-show="!useredit_form.username.$dirty && useredit.error.username">{{ useredit.error.username }}</small>
|
||||
<div class="control-label" ng-show="(!useredit_form.username.$dirty && userEdit.error.username) || (useredit_form.username.$dirty && useredit_form.username.$invalid) || (!useredit_form.username.$dirty && userEdit.error.username)">
|
||||
<small ng-show="!useredit_form.username.$dirty && userEdit.error.username">{{ userEdit.error.username }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="useredit.username" name="username" autocomplete="off">
|
||||
<input type="text" class="form-control" ng-model="userEdit.username" name="username" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group" ng-hide="useredit.source" ng-class="{ 'has-error': (useredit_form.displayName.$dirty && useredit_form.displayName.$invalid) || (!useredit_form.displayName.$dirty && useredit.error.displayName) }">
|
||||
<div class="form-group" ng-hide="userEdit.source" ng-class="{ 'has-error': (useredit_form.displayName.$dirty && useredit_form.displayName.$invalid) || (!useredit_form.displayName.$dirty && userEdit.error.displayName) }">
|
||||
<label class="control-label">{{ 'users.user.displayName' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!useredit_form.displayName.$dirty && useredit.error.displayName) || (useredit_form.displayName.$dirty && useredit_form.displayName.$invalid) || (!useredit_form.displayName.$dirty && useredit.error.displayName)">
|
||||
<div class="control-label" ng-show="(!useredit_form.displayName.$dirty && userEdit.error.displayName) || (useredit_form.displayName.$dirty && useredit_form.displayName.$invalid) || (!useredit_form.displayName.$dirty && userEdit.error.displayName)">
|
||||
<small ng-show="useredit_form.displayName.$error.required">{{ 'users.user.errorDisplayNameRequired' | tr }}</small>
|
||||
<small ng-show="!useredit_form.displayName.$dirty && useredit.error.displayName">{{ useredit.error.displayName }}</small>
|
||||
<small ng-show="!useredit_form.displayName.$dirty && userEdit.error.displayName">{{ userEdit.error.displayName }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="useredit.displayName" name="displayName" required autofocus autocomplete="off">
|
||||
<input type="text" class="form-control" ng-model="userEdit.displayName" name="displayName" required autofocus autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group" ng-hide="useredit.source" ng-class="{ 'has-error': (useredit_form.email.$dirty && useredit_form.email.$invalid) || (!useredit_form.email.$dirty && useredit.error.email) }">
|
||||
<div class="form-group" ng-hide="userEdit.source" ng-class="{ 'has-error': (useredit_form.email.$dirty && useredit_form.email.$invalid) || (!useredit_form.email.$dirty && userEdit.error.email) }">
|
||||
<label class="control-label">{{ 'users.user.primaryEmail' | tr }} <sup><a ng-href="https://docs.cloudron.io/profile/#primary-email" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></label>
|
||||
<div class="control-label" ng-show="(!useredit_form.email.$dirty && useredit.error.email) || (useredit_form.email.$dirty && useredit_form.email.$invalid) || (!useredit_form.email.$dirty && useredit.error.email)">
|
||||
<div class="control-label" ng-show="(!useredit_form.email.$dirty && userEdit.error.email) || (useredit_form.email.$dirty && useredit_form.email.$invalid) || (!useredit_form.email.$dirty && userEdit.error.email)">
|
||||
<small ng-show="useredit_form.email.$error.required">{{ 'users.user.errorEmailRequired' | tr }}</small>
|
||||
<small ng-show="useredit_form.email.$error.email">{{ 'users.user.errorInvalidEmail' | tr }}</small>
|
||||
<small ng-show="!useredit_form.email.$dirty && useredit.error.email">{{ useredit.error.email }}</small>
|
||||
<small ng-show="!useredit_form.email.$dirty && userEdit.error.email">{{ userEdit.error.email }}</small>
|
||||
</div>
|
||||
<input type="email" class="form-control" ng-model="useredit.email" name="email" required>
|
||||
<input type="email" class="form-control" ng-model="userEdit.email" name="email" required>
|
||||
</div>
|
||||
<div class="form-group" ng-hide="useredit.source" ng-class="{ 'has-error': (useredit_form.fallbackEmail.$dirty && useredit_form.fallbackEmail.$invalid) || (!useredit_form.fallbackEmail.$dirty && useredit.error.fallbackEmail) }">
|
||||
<div class="form-group" ng-hide="userEdit.source" ng-class="{ 'has-error': (useredit_form.fallbackEmail.$dirty && useredit_form.fallbackEmail.$invalid) || (!useredit_form.fallbackEmail.$dirty && userEdit.error.fallbackEmail) }">
|
||||
<label class="control-label">{{ 'users.user.recoveryEmail' | tr }} <sup><a ng-href="https://docs.cloudron.io/profile/#password-recovery-email" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></label>
|
||||
<div class="control-label" ng-show="(!useredit_form.fallbackEmail.$dirty && useredit.error.fallbackEmail) || (useredit_form.fallbackEmail.$dirty && useredit_form.fallbackEmail.$invalid) || (!useredit_form.fallbackEmail.$dirty && useredit.error.fallbackEmail)">
|
||||
<div class="control-label" ng-show="(!useredit_form.fallbackEmail.$dirty && userEdit.error.fallbackEmail) || (useredit_form.fallbackEmail.$dirty && useredit_form.fallbackEmail.$invalid) || (!useredit_form.fallbackEmail.$dirty && userEdit.error.fallbackEmail)">
|
||||
<small ng-show="useredit_form.fallbackEmail.$error.fallbackEmail">{{ 'users.user.errorInvalidEmail' | tr }}</small>
|
||||
<small ng-show="!useredit_form.fallbackEmail.$dirty && useredit.error.fallbackEmail">{{ useredit.error.fallbackEmail }}</small>
|
||||
<small ng-show="!useredit_form.fallbackEmail.$dirty && userEdit.error.fallbackEmail">{{ userEdit.error.fallbackEmail }}</small>
|
||||
</div>
|
||||
<input type="fallbackEmail" class="form-control" ng-model="useredit.fallbackEmail" name="fallbackEmail">
|
||||
<input type="fallbackEmail" class="form-control" ng-model="userEdit.fallbackEmail" name="fallbackEmail">
|
||||
</div>
|
||||
<div class="form-group" ng-show="!isMe(useredit.userInfo) && userInfo.isAtLeastAdmin">
|
||||
<div class="form-group" ng-show="!isMe(userEdit.userInfo) && userInfo.isAtLeastAdmin">
|
||||
<label class="control-label">{{ 'users.user.role' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-management/#roles" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<div class="control-label">
|
||||
<select class="form-control" ng-model="useredit.role" ng-options="role.id as role.name disable when role.disabled for role in roles"></select>
|
||||
<select class="form-control" ng-model="userEdit.role" ng-options="role.id as role.name disable when role.disabled for role in roles"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'users.user.groups' | tr }}</label>
|
||||
<div class="control-label">
|
||||
<div ng-show="groups.length === 0">{{ 'users.user.noGroups' | tr }}</div>
|
||||
<multiselect ng-show="groups.length !== 0" ng-model="useredit.selectedGroups" options="group.name for group in groups" data-compare-by="id" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<div ng-switch on="groups.length">
|
||||
<div ng-switch-when="0">{{ 'users.user.noGroups' | tr }}</div>
|
||||
<div ng-switch-default>
|
||||
<!-- local groups. they can have local and external users . angular cannot filter empty strings - https://github.com/angular/angular.js/issues/7890 -->
|
||||
<multiselect ng-show="hasLocalGroups" ng-model="userEdit.selectedLocalGroups" options="group.name for group in groups | filter:{ source: '!ldap' }" data-compare-by="id" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-hide="isMe(useredit.userInfo)">
|
||||
<div class="form-group" ng-show="userEdit.externalGroups.length">
|
||||
<!-- remote groups. cannot be edited -->
|
||||
<label class="control-label">{{ 'users.user.ldapGroups' | tr }}</label>
|
||||
<div><span ng-repeat="group in userEdit.externalGroups">{{ group.name }}</span></div>
|
||||
</div>
|
||||
<div class="form-group" ng-hide="isMe(userEdit.userInfo)">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="useredit.active"> {{ 'users.user.activeCheckbox' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-management/#disable-user" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup>
|
||||
<input type="checkbox" ng-model="userEdit.active"> {{ 'users.user.activeCheckbox' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-management/#disable-user" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<input class="hide" type="submit" ng-disabled="useredit_form.$invalid || useredit.busy"/>
|
||||
<input class="hide" type="submit" ng-disabled="useredit_form.$invalid || userEdit.busy"/>
|
||||
</form>
|
||||
<hr/>
|
||||
<div>
|
||||
<p ng-hide="useredit.userInfo.twoFactorAuthenticationEnabled">{{ 'users.passwordResetDialog.no2FASetup' | tr }}</p>
|
||||
<p ng-show="useredit.userInfo.twoFactorAuthenticationEnabled">{{ 'users.passwordResetDialog.2FAIsSetup' | tr }}</p>
|
||||
<button type="button" class="btn btn-danger" ng-click="useredit.reset2FA()" ng-disabled="!useredit.userInfo.twoFactorAuthenticationEnabled || useredit.reset2FABusy"><i class="fa fa-circle-notch fa-spin" ng-show="useredit.reset2FABusy"></i> {{ 'users.passwordResetDialog.reset2FAAction' | tr }}</button>
|
||||
<div ng-hide="userEdit.source && config.external2FA">
|
||||
<p ng-hide="userEdit.userInfo.twoFactorAuthenticationEnabled">{{ 'users.passwordResetDialog.no2FASetup' | tr }}</p>
|
||||
<p ng-show="userEdit.userInfo.twoFactorAuthenticationEnabled">{{ 'users.passwordResetDialog.2FAIsSetup' | tr }}</p>
|
||||
<button type="button" class="btn btn-danger" ng-click="userEdit.reset2FA()" ng-disabled="!userEdit.userInfo.twoFactorAuthenticationEnabled || userEdit.reset2FABusy"><i class="fa fa-circle-notch fa-spin" ng-show="userEdit.reset2FABusy"></i> {{ 'users.passwordResetDialog.reset2FAAction' | tr }}</button>
|
||||
</div>
|
||||
<div ng-show="userEdit.source && config.external2FA"> {{ 'users.user.external2FA' | tr }}</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="useredit.submit()" ng-disabled="useredit_form.$invalid || useredit.busy"><i class="fa fa-circle-notch fa-spin" ng-show="useredit.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="userEdit.submit()" ng-disabled="useredit_form.$invalid || userEdit.busy"><i class="fa fa-circle-notch fa-spin" ng-show="userEdit.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -256,7 +249,8 @@
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'users.group.users' | tr }}</label>
|
||||
<div class="control-label">
|
||||
<multiselect ng-model="groupEdit.selectedUsers" options="(user.username || user.email) for user in allUsers" data-compare-by="email" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<multiselect ng-hide="groupEdit.source" ng-model="groupEdit.selectedUsers" ng-disabled="groupEdit.busy" options="(user.username || user.email) for user in allUsers" data-compare-by="email" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<div ng-show="groupEdit.source"><span ng-repeat="user in groupEdit.selectedUsers"> {{ (user.username || user.email) }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -265,7 +259,7 @@
|
||||
<multiselect ng-model="groupEdit.selectedApps" options="(app.label || app.fqdn) for app in groupEdit.apps" data-compare-by="fqdn" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
<input class="hide" type="submit" ng-disabled="groupEdit_form.$invalid || useredit.busy"/>
|
||||
<input class="hide" type="submit" ng-disabled="groupEdit_form.$invalid || groupEdit.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -460,7 +454,8 @@
|
||||
<input type="text" id="userSearchInput" class="form-control" style="max-width: 350px;" ng-model="userSearchString" ng-model-options="{ debounce: 1000 }" ng-change="updateFilter()" placeholder="{{ 'main.searchPlaceholder' | tr }}"/>
|
||||
<multiselect ng-model="userStateFilter" ms-header="{{ 'apps.stateFilterHeader' | tr }}" ms-selected="{{ userStateFilter }}" options="state.label for state in userStates" data-multiple="false"></multiselect>
|
||||
<div style="flex-grow: 1;"></div>
|
||||
<div class="btn-group">
|
||||
<!-- import/export buttons are hidden until we figure what the exact use case is -->
|
||||
<div class="btn-group" ng-hide="true">
|
||||
<button class="btn btn-default" ng-click="userImport.show()" uib-tooltip="{{ 'users.userImport.tooltip' | tr }}" tooltip-append-to-body="true"><i class="fas fa-download"></i></button>
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown" uib-tooltip="{{ 'users.userExport.tooltip' | tr }}" tooltip-append-to-body="true">
|
||||
@@ -472,7 +467,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-outline" ng-click="useradd.show()">
|
||||
<button class="btn btn-primary btn-outline" ng-click="userAdd.show()">
|
||||
<i class="fa fa-user-plus"></i> {{ 'users.newUserAction' | tr }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -506,13 +501,13 @@
|
||||
<i class="fas fa-mail-bulk arrow" ng-show="user.active && user.role === 'mailmanager'" uib-tooltip="{{ 'users.users.mailmanagerTooltip' | tr }}"></i>
|
||||
<i class="fa fa-ban" ng-show="!user.active" uib-tooltip="{{ 'users.users.inactiveTooltip' | tr }}"></i>
|
||||
</td>
|
||||
<td class="hand elide-table-cell" ng-click="canEdit(user) && useredit.show(user)" ng-show="user.username">
|
||||
<td class="hand elide-table-cell" ng-click="canEdit(user) && userEdit.show(user)" ng-show="user.username">
|
||||
{{ user.displayName }} <span class="text-muted">{{ user.username }}</span> <i ng-show="user.source" class="far fa-address-book" uib-tooltip="{{ 'users.users.externalLdapTooltip' | tr }}"></i>
|
||||
</td>
|
||||
<td class="hand elide-table-cell" ng-click="canEdit(user) && useredit.show(user)" ng-hide="user.username">
|
||||
<td class="hand elide-table-cell" ng-click="canEdit(user) && userEdit.show(user)" ng-hide="user.username">
|
||||
<span class="text-muted" uib-tooltip="{{ 'users.users.notActivatedYetTooltip' | tr }}">{{ user.email }}</span>
|
||||
</td>
|
||||
<td class="text-left hand elide-table-cell hidden-xs hidden-sm" ng-click="canEdit(user) && useredit.show(user)">
|
||||
<td class="text-left hand elide-table-cell hidden-xs hidden-sm" ng-click="canEdit(user) && userEdit.show(user)">
|
||||
<span class="group-badge" ng-repeat="groupId in user.groupIds">
|
||||
{{ groupsById[groupId].name }}
|
||||
</span>
|
||||
@@ -520,11 +515,10 @@
|
||||
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button ng-disabled="!canEdit(user)" ng-show="!user.inviteAccepted && !isMe(user) && !user.source" class="btn btn-xs btn-default" ng-click="invitation.show(user)" uib-tooltip="{{ 'users.users.invitationTooltip' | tr }}"><i class="fas fa-paper-plane"></i></button>
|
||||
<button ng-show="user.source" class="btn btn-xs btn-default" ng-click="makeLocal.show(user)" uib-tooltip="{{ 'users.users.makeLocalTooltip' | tr }}"><i class="fas fa-thumbtack" style="width: 10.5px;"></i></button>
|
||||
<button ng-disabled="!canEdit(user)" ng-show="user.inviteAccepted && !user.source" class="btn btn-xs btn-default" ng-click="passwordReset.show(user)" uib-tooltip="{{ 'users.users.resetPasswordTooltip' | tr }}"><i class="fas fa-key"></i></button>
|
||||
<button ng-disabled="!canImpersonate(user)" class="btn btn-xs btn-default" ng-click="setGhost.show(user)" uib-tooltip="{{ 'users.users.setGhostTooltip' | tr }}"><i class="fas fa-user-secret"></i></button>
|
||||
<button ng-disabled="!canEdit(user)" class="btn btn-xs btn-default" ng-click="useredit.show(user)" uib-tooltip="{{ 'users.users.editUserTooltip' | tr }}"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button ng-disabled="!canEdit(user) || isMe(user)" class="btn btn-xs btn-danger" ng-click="userremove.show(user)" uib-tooltip="{{ 'users.users.removeUserTooltip' | tr }}"><i class="far fa-trash-alt"></i></button>
|
||||
<button ng-disabled="!canEdit(user)" class="btn btn-xs btn-default" ng-click="userEdit.show(user)" uib-tooltip="{{ 'users.users.editUserTooltip' | tr }}"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button ng-disabled="!canEdit(user) || isMe(user)" class="btn btn-xs btn-danger" ng-click="userRemove.show(user)" uib-tooltip="{{ 'users.users.removeUserTooltip' | tr }}"><i class="far fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
+161
-178
@@ -13,6 +13,7 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
$scope.users = []; // users of current page
|
||||
$scope.allUsersById = [];
|
||||
$scope.groups = [];
|
||||
$scope.hasLocalGroups = false;
|
||||
$scope.groupsById = { };
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.userInfo = Client.getUserInfo();
|
||||
@@ -186,7 +187,7 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
$scope.userImport.busy = false;
|
||||
$scope.userImport.done = true;
|
||||
if ($scope.userImport.success) {
|
||||
refresh();
|
||||
refreshCurrentPage();
|
||||
refreshAllUsers();
|
||||
}
|
||||
});
|
||||
@@ -231,30 +232,30 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
});
|
||||
};
|
||||
|
||||
$scope.userremove = {
|
||||
$scope.userRemove = {
|
||||
busy: false,
|
||||
error: null,
|
||||
userInfo: {},
|
||||
|
||||
show: function (userInfo) {
|
||||
$scope.userremove.error = null;
|
||||
$scope.userremove.userInfo = userInfo;
|
||||
$scope.userRemove.error = null;
|
||||
$scope.userRemove.userInfo = userInfo;
|
||||
|
||||
$('#userRemoveModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.userremove.busy = true;
|
||||
$scope.userRemove.busy = true;
|
||||
|
||||
Client.removeUser($scope.userremove.userInfo.id, function (error) {
|
||||
$scope.userremove.busy = false;
|
||||
Client.removeUser($scope.userRemove.userInfo.id, function (error) {
|
||||
$scope.userRemove.busy = false;
|
||||
|
||||
if (error && error.statusCode === 403) return $scope.userremove.error = error.message;
|
||||
if (error && error.statusCode === 403) return $scope.userRemove.error = error.message;
|
||||
else if (error) return console.error('Unable to delete user.', error);
|
||||
|
||||
$scope.userremove.userInfo = {};
|
||||
$scope.userRemove.userInfo = {};
|
||||
|
||||
refresh();
|
||||
refreshCurrentPage();
|
||||
refreshAllUsers();
|
||||
|
||||
$('#userRemoveModal').modal('hide');
|
||||
@@ -262,7 +263,7 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
}
|
||||
};
|
||||
|
||||
$scope.useradd = {
|
||||
$scope.userAdd = {
|
||||
busy: false,
|
||||
alreadyTaken: false,
|
||||
error: {},
|
||||
@@ -270,19 +271,19 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
fallbackEmail: '',
|
||||
username: '',
|
||||
displayName: '',
|
||||
selectedGroups: [],
|
||||
selectedLocalGroups: [],
|
||||
role: 'user',
|
||||
sendInvite: false,
|
||||
|
||||
show: function () {
|
||||
$scope.useradd.error = {};
|
||||
$scope.useradd.email = '';
|
||||
$scope.useradd.fallbackEmail = '';
|
||||
$scope.useradd.username = '';
|
||||
$scope.useradd.displayName = '';
|
||||
$scope.useradd.selectedGroups = [];
|
||||
$scope.useradd.role = 'user';
|
||||
$scope.useradd.sendInvite = false;
|
||||
$scope.userAdd.error = {};
|
||||
$scope.userAdd.email = '';
|
||||
$scope.userAdd.fallbackEmail = '';
|
||||
$scope.userAdd.username = '';
|
||||
$scope.userAdd.displayName = '';
|
||||
$scope.userAdd.selectedLocalGroups = [];
|
||||
$scope.userAdd.role = 'user';
|
||||
$scope.userAdd.sendInvite = false;
|
||||
|
||||
$scope.useraddForm.$setUntouched();
|
||||
$scope.useraddForm.$setPristine();
|
||||
@@ -291,33 +292,33 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.useradd.busy = true;
|
||||
$scope.userAdd.busy = true;
|
||||
|
||||
$scope.useradd.alreadyTaken = false;
|
||||
$scope.useradd.error.email = null;
|
||||
$scope.useradd.error.fallbackEmail = null;
|
||||
$scope.useradd.error.username = null;
|
||||
$scope.useradd.error.displayName = null;
|
||||
$scope.userAdd.alreadyTaken = false;
|
||||
$scope.userAdd.error.email = null;
|
||||
$scope.userAdd.error.fallbackEmail = null;
|
||||
$scope.userAdd.error.username = null;
|
||||
$scope.userAdd.error.displayName = null;
|
||||
|
||||
var user = {
|
||||
username: $scope.useradd.username || null,
|
||||
email: $scope.useradd.email,
|
||||
fallbackEmail: $scope.useradd.fallbackEmail,
|
||||
displayName: $scope.useradd.displayName,
|
||||
role: $scope.useradd.role
|
||||
username: $scope.userAdd.username || null,
|
||||
email: $scope.userAdd.email,
|
||||
fallbackEmail: $scope.userAdd.fallbackEmail,
|
||||
displayName: $scope.userAdd.displayName,
|
||||
role: $scope.userAdd.role
|
||||
};
|
||||
|
||||
Client.addUser(user, function (error, userId) {
|
||||
if (error) {
|
||||
$scope.useradd.busy = false;
|
||||
$scope.userAdd.busy = false;
|
||||
|
||||
if (error.statusCode === 409) {
|
||||
if (error.message.toLowerCase().indexOf('email') !== -1) {
|
||||
$scope.useradd.error.email = 'Email already taken';
|
||||
$scope.userAdd.error.email = 'Email already taken';
|
||||
$scope.useraddForm.email.$setPristine();
|
||||
$('#inputUserAddEmail').focus();
|
||||
} else if (error.message.toLowerCase().indexOf('username') !== -1 || error.message.toLowerCase().indexOf('mailbox') !== -1) {
|
||||
$scope.useradd.error.username = 'Username already taken';
|
||||
$scope.userAdd.error.username = 'Username already taken';
|
||||
$scope.useraddForm.username.$setPristine();
|
||||
$('#inputUserAddUsername').focus();
|
||||
} else {
|
||||
@@ -327,12 +328,12 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
return;
|
||||
} else if (error.statusCode === 400) {
|
||||
if (error.message.toLowerCase().indexOf('email') !== -1) {
|
||||
$scope.useradd.error.email = 'Invalid Email';
|
||||
$scope.useradd.error.emailAttempted = $scope.useradd.email;
|
||||
$scope.userAdd.error.email = 'Invalid Email';
|
||||
$scope.userAdd.error.emailAttempted = $scope.userAdd.email;
|
||||
$scope.useraddForm.email.$setPristine();
|
||||
$('#inputUserAddEmail').focus();
|
||||
} else if (error.message.toLowerCase().indexOf('username') !== -1) {
|
||||
$scope.useradd.error.username = error.message;
|
||||
$scope.userAdd.error.username = error.message;
|
||||
$scope.useraddForm.username.$setPristine();
|
||||
$('#inputUserAddUsername').focus();
|
||||
} else {
|
||||
@@ -344,16 +345,16 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
}
|
||||
}
|
||||
|
||||
var groupIds = $scope.useradd.selectedGroups.map(function (g) { return g.id; });
|
||||
var localGroupIds = $scope.userAdd.selectedLocalGroups.map(function (g) { return g.id; });
|
||||
|
||||
Client.setGroups(userId, groupIds, function (error) {
|
||||
$scope.useradd.busy = false;
|
||||
Client.setLocalGroups(userId, localGroupIds, function (error) {
|
||||
$scope.userAdd.busy = false;
|
||||
|
||||
if (error) return console.error(error);
|
||||
|
||||
if ($scope.useradd.sendInvite) Client.sendInviteEmail(userId, user.email, function (error) { if (error) console.error('Failed to send invite.', error); });
|
||||
if ($scope.userAdd.sendInvite) Client.sendInviteEmail(userId, user.email, function (error) { if (error) console.error('Failed to send invite.', error); });
|
||||
|
||||
refresh();
|
||||
refreshCurrentPage();
|
||||
refreshAllUsers();
|
||||
|
||||
$('#userAddModal').modal('hide');
|
||||
@@ -362,7 +363,7 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
}
|
||||
};
|
||||
|
||||
$scope.useredit = {
|
||||
$scope.userEdit = {
|
||||
busy: false,
|
||||
reset2FABusy: false,
|
||||
error: {},
|
||||
@@ -376,20 +377,22 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
displayName: '',
|
||||
active: false,
|
||||
source: '',
|
||||
selectedGroups: [],
|
||||
selectedLocalGroups: [],
|
||||
externalGroups: [],
|
||||
role: '',
|
||||
|
||||
show: function (userInfo) {
|
||||
$scope.useredit.error = {};
|
||||
$scope.useredit.username = userInfo.username;
|
||||
$scope.useredit.email = userInfo.email;
|
||||
$scope.useredit.displayName = userInfo.displayName;
|
||||
$scope.useredit.fallbackEmail = userInfo.fallbackEmail;
|
||||
$scope.useredit.userInfo = userInfo;
|
||||
$scope.useredit.selectedGroups = userInfo.groupIds.map(function (gid) { return $scope.groupsById[gid]; });
|
||||
$scope.useredit.active = userInfo.active;
|
||||
$scope.useredit.source = userInfo.source;
|
||||
$scope.useredit.role = userInfo.role;
|
||||
$scope.userEdit.error = {};
|
||||
$scope.userEdit.username = userInfo.username;
|
||||
$scope.userEdit.email = userInfo.email;
|
||||
$scope.userEdit.displayName = userInfo.displayName;
|
||||
$scope.userEdit.fallbackEmail = userInfo.fallbackEmail;
|
||||
$scope.userEdit.userInfo = userInfo;
|
||||
$scope.userEdit.selectedLocalGroups = userInfo.groupIds.map(function (gid) { return $scope.groupsById[gid]; }).filter(function (g) { return g.source === ''; });
|
||||
$scope.userEdit.externalGroups = userInfo.groupIds.map(function (gid) { return $scope.groupsById[gid]; }).filter(function (g) { return g.source !== ''; });
|
||||
$scope.userEdit.active = userInfo.active;
|
||||
$scope.userEdit.source = userInfo.source;
|
||||
$scope.userEdit.role = userInfo.role;
|
||||
|
||||
$scope.useredit_form.$setPristine();
|
||||
$scope.useredit_form.$setUntouched();
|
||||
@@ -398,72 +401,69 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.useredit.error = {};
|
||||
$scope.useredit.busy = true;
|
||||
$scope.userEdit.error = {};
|
||||
$scope.userEdit.busy = true;
|
||||
|
||||
var userId = $scope.useredit.userInfo.id;
|
||||
var data = {
|
||||
id: userId
|
||||
};
|
||||
var userId = $scope.userEdit.userInfo.id;
|
||||
|
||||
// only send if not the current active user
|
||||
if (userId !== $scope.userInfo.id) {
|
||||
data.active = $scope.useredit.active;
|
||||
data.role = $scope.useredit.role;
|
||||
}
|
||||
async.series([
|
||||
function setRole(next) {
|
||||
if (userId === $scope.userInfo.id) return next(); // cannot set role on self
|
||||
Client.setRole(userId, $scope.userEdit.role, next);
|
||||
},
|
||||
function setActive(next) {
|
||||
if (userId === $scope.userInfo.id) return next(); // cannot set role on self
|
||||
Client.setActive(userId, $scope.userEdit.active, next);
|
||||
},
|
||||
function updateUserProfile(next) {
|
||||
if ($scope.userEdit.source) return next(); // cannot update profile of external user
|
||||
// username is settable only if it was empty previously. it's editable for the "lock" profiles feature
|
||||
var data = {};
|
||||
if (!$scope.userEdit.userInfo.username) data.username = $scope.userEdit.username;
|
||||
data.email = $scope.userEdit.email;
|
||||
data.displayName = $scope.userEdit.displayName;
|
||||
data.fallbackEmail = $scope.userEdit.fallbackEmail;
|
||||
Client.updateUserProfile(userId, data, next);
|
||||
},
|
||||
function setLocalGroups(next) {
|
||||
var localGroupIds = $scope.userEdit.selectedLocalGroups.map(function (g) { return g.id; });
|
||||
Client.setLocalGroups(userId, localGroupIds, next);
|
||||
}
|
||||
], function (error) {
|
||||
$scope.userEdit.busy = false;
|
||||
|
||||
// only change those if it is a local user
|
||||
if (!$scope.useredit.source) {
|
||||
// username is settable only if it was empty previously. it's editable for the "lock" profiles feature
|
||||
if (!$scope.useredit.userInfo.username) data.username = $scope.useredit.username;
|
||||
data.email = $scope.useredit.email;
|
||||
data.displayName = $scope.useredit.displayName;
|
||||
data.fallbackEmail = $scope.useredit.fallbackEmail;
|
||||
}
|
||||
|
||||
Client.updateUser(data, function (error) {
|
||||
if (error) {
|
||||
$scope.useredit.busy = false;
|
||||
|
||||
if (error.statusCode === 409) {
|
||||
if (error.message.toLowerCase().indexOf('email') !== -1) {
|
||||
$scope.useredit.error.email = 'Email already taken';
|
||||
$scope.userEdit.error.email = 'Email already taken';
|
||||
} else if (error.message.toLowerCase().indexOf('username') !== -1) {
|
||||
$scope.useredit.error.username = 'Username already taken';
|
||||
$scope.userEdit.error.username = 'Username already taken';
|
||||
}
|
||||
$scope.useredit_form.email.$setPristine();
|
||||
$('#inputUserEditEmail').focus();
|
||||
} else {
|
||||
$scope.useredit.error.generic = error.message;
|
||||
$scope.userEdit.error.generic = error.message;
|
||||
console.error('Unable to update user:', error);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var groupIds = $scope.useredit.selectedGroups.map(function (g) { return g.id; });
|
||||
|
||||
Client.setGroups(data.id, groupIds, function (error) {
|
||||
$scope.useredit.busy = false;
|
||||
|
||||
if (error) return console.error('Unable to update groups for user:', error);
|
||||
|
||||
refreshUsers(false);
|
||||
|
||||
$('#userEditModal').modal('hide');
|
||||
});
|
||||
refreshUsersCurrentPage(false /* busy indicator */);
|
||||
refreshGroups();
|
||||
$('#userEditModal').modal('hide');
|
||||
});
|
||||
},
|
||||
|
||||
reset2FA: function () {
|
||||
$scope.useredit.reset2FABusy = true;
|
||||
$scope.userEdit.reset2FABusy = true;
|
||||
|
||||
Client.disableTwoFactorAuthenticationByUserId($scope.useredit.userInfo.id, function (error) {
|
||||
Client.disableTwoFactorAuthenticationByUserId($scope.userEdit.userInfo.id, function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$timeout(function () {
|
||||
$scope.useredit.userInfo.twoFactorAuthenticationEnabled = false;
|
||||
$scope.useredit.reset2FABusy = false;
|
||||
$scope.userEdit.userInfo.twoFactorAuthenticationEnabled = false;
|
||||
$scope.userEdit.reset2FABusy = false;
|
||||
}, 3000);
|
||||
});
|
||||
}
|
||||
@@ -480,6 +480,7 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
|
||||
$scope.groupAdd.error = {};
|
||||
$scope.groupAdd.name = '';
|
||||
$scope.groupAdd.selectedUsers = [];
|
||||
|
||||
$scope.groupAddForm.$setUntouched();
|
||||
$scope.groupAddForm.$setPristine();
|
||||
@@ -517,7 +518,7 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
|
||||
if (error) return console.error('Unable to add memebers.', error.statusCode, error.message);
|
||||
|
||||
refresh();
|
||||
refreshCurrentPage();
|
||||
|
||||
$('#groupAddModal').modal('hide');
|
||||
});
|
||||
@@ -556,11 +557,61 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
$('#groupEditModal').modal('show');
|
||||
},
|
||||
|
||||
updateAccessRestriction: function () {
|
||||
// find apps where ACL has changed
|
||||
var addedApps = $scope.groupEdit.selectedApps.filter(function (a) {
|
||||
return !$scope.groupEdit.selectedAppsOriginal.find(function (b) { return b.id === a.id; });
|
||||
});
|
||||
var removedApps = $scope.groupEdit.selectedAppsOriginal.filter(function (a) {
|
||||
return !$scope.groupEdit.selectedApps.find(function (b) { return b.id === a.id; });
|
||||
});
|
||||
|
||||
async.eachSeries(addedApps, function (app, callback) {
|
||||
var accessRestriction = app.accessRestriction;
|
||||
if (!accessRestriction) accessRestriction = { users: [], groups: [] };
|
||||
if (!Array.isArray(accessRestriction.groups)) accessRestriction.groups = [];
|
||||
|
||||
accessRestriction.groups.push($scope.groupEdit.groupInfo.id);
|
||||
|
||||
Client.configureApp(app.id, 'access_restriction', { accessRestriction: accessRestriction }, callback);
|
||||
}, function (error) {
|
||||
if (error) {
|
||||
$scope.groupEdit.busy = false;
|
||||
return console.error('Unable to set added app access.', error.statusCode, error.message);
|
||||
}
|
||||
|
||||
async.eachSeries(removedApps, function (app, callback) {
|
||||
var accessRestriction = app.accessRestriction;
|
||||
if (!accessRestriction) accessRestriction = { users: [], groups: [] };
|
||||
if (!Array.isArray(accessRestriction.groups)) accessRestriction.groups = [];
|
||||
|
||||
var deleted = accessRestriction.groups.splice(accessRestriction.groups.indexOf($scope.groupEdit.groupInfo.id), 1);
|
||||
|
||||
// if not found return early
|
||||
if (deleted.length === 0) return callback();
|
||||
|
||||
Client.configureApp(app.id, 'access_restriction', { accessRestriction: accessRestriction }, callback);
|
||||
}, function (error) {
|
||||
$scope.groupEdit.busy = false;
|
||||
if (error) return console.error('Unable to set removed app access.', error.statusCode, error.message);
|
||||
|
||||
refreshCurrentPage();
|
||||
|
||||
// refresh apps to reflect change
|
||||
Client.refreshInstalledApps();
|
||||
|
||||
$('#groupEditModal').modal('hide');
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.groupEdit.busy = true;
|
||||
$scope.groupEdit.error = {};
|
||||
|
||||
Client.updateGroup($scope.groupEdit.groupInfo.id, $scope.groupEdit.name, function (error) {
|
||||
if ($scope.groupEdit.source) return $scope.groupEdit.updateAccessRestriction(); // cannot update name or members of external groups
|
||||
|
||||
Client.setGroupName($scope.groupEdit.groupInfo.id, $scope.groupEdit.name, function (error) {
|
||||
if (error) {
|
||||
$scope.groupEdit.busy = false;
|
||||
|
||||
@@ -587,51 +638,7 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
return console.error('Unable to set group members.', error.statusCode, error.message);
|
||||
}
|
||||
|
||||
// find apps where ACL has changed
|
||||
var addedApps = $scope.groupEdit.selectedApps.filter(function (a) {
|
||||
return !$scope.groupEdit.selectedAppsOriginal.find(function (b) { return b.id === a.id; });
|
||||
});
|
||||
var removedApps = $scope.groupEdit.selectedAppsOriginal.filter(function (a) {
|
||||
return !$scope.groupEdit.selectedApps.find(function (b) { return b.id === a.id; });
|
||||
});
|
||||
|
||||
async.eachSeries(addedApps, function (app, callback) {
|
||||
var accessRestriction = app.accessRestriction;
|
||||
if (!accessRestriction) accessRestriction = { users: [], groups: [] };
|
||||
if (!Array.isArray(accessRestriction.groups)) accessRestriction.groups = [];
|
||||
|
||||
accessRestriction.groups.push($scope.groupEdit.groupInfo.id);
|
||||
|
||||
Client.configureApp(app.id, 'access_restriction', { accessRestriction: accessRestriction }, callback);
|
||||
}, function (error) {
|
||||
if (error) {
|
||||
$scope.groupEdit.busy = false;
|
||||
return console.error('Unable to set added app access.', error.statusCode, error.message);
|
||||
}
|
||||
|
||||
async.eachSeries(removedApps, function (app, callback) {
|
||||
var accessRestriction = app.accessRestriction;
|
||||
if (!accessRestriction) accessRestriction = { users: [], groups: [] };
|
||||
if (!Array.isArray(accessRestriction.groups)) accessRestriction.groups = [];
|
||||
|
||||
var deleted = accessRestriction.groups.splice(accessRestriction.groups.indexOf($scope.groupEdit.groupInfo.id), 1);
|
||||
|
||||
// if not found return early
|
||||
if (deleted.length === 0) return callback();
|
||||
|
||||
Client.configureApp(app.id, 'access_restriction', { accessRestriction: accessRestriction }, callback);
|
||||
}, function (error) {
|
||||
$scope.groupEdit.busy = false;
|
||||
if (error) return console.error('Unable to set removed app access.', error.statusCode, error.message);
|
||||
|
||||
refresh();
|
||||
|
||||
// refresh apps to reflect change
|
||||
Client.refreshInstalledApps();
|
||||
|
||||
$('#groupEditModal').modal('hide');
|
||||
});
|
||||
});
|
||||
$scope.groupEdit.updateAccessRestriction();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -668,7 +675,7 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
|
||||
if (error) return console.error('Unable to remove group.', error.statusCode, error.message);
|
||||
|
||||
refresh();
|
||||
refreshCurrentPage();
|
||||
$('#groupRemoveModal').modal('hide');
|
||||
});
|
||||
}
|
||||
@@ -717,32 +724,6 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
}
|
||||
};
|
||||
|
||||
$scope.makeLocal = {
|
||||
busy: false,
|
||||
user: null,
|
||||
|
||||
show: function (user) {
|
||||
$scope.makeLocal.busy = false;
|
||||
$scope.makeLocal.user = user;
|
||||
|
||||
$('#makeLocalModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.makeLocal.busy = false;
|
||||
|
||||
Client.makeUserLocal($scope.makeLocal.user.id, function (error) {
|
||||
if (error) return console.error('Failed to make user local.', error);
|
||||
|
||||
$scope.makeLocal.busy = false;
|
||||
|
||||
refreshUsers();
|
||||
|
||||
$('#makeLocalModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.invitation = {
|
||||
busy: false,
|
||||
inviteLink: '',
|
||||
@@ -825,7 +806,7 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
}
|
||||
};
|
||||
|
||||
function getUsers(callback) {
|
||||
function getUsersCurrentPage(callback) {
|
||||
var users = [];
|
||||
|
||||
Client.getUsers($scope.userSearchString, $scope.userStateFilter.value, $scope.currentPage, $scope.pageItems, function (error, results) {
|
||||
@@ -845,10 +826,10 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
});
|
||||
}
|
||||
|
||||
function refreshUsers(showBusy) { // loads users on current page only
|
||||
function refreshUsersCurrentPage(showBusy) { // loads users on current page only
|
||||
if (showBusy) $scope.userRefreshBusy = true;
|
||||
|
||||
getUsers(function (error, result) {
|
||||
getUsersCurrentPage(function (error, result) {
|
||||
if (error) return console.error('Unable to get user listing.', error);
|
||||
|
||||
angular.copy(result, $scope.users);
|
||||
@@ -867,34 +848,36 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
|
||||
angular.copy(result, $scope.groups);
|
||||
$scope.groupsById = { };
|
||||
$scope.hasLocalGroups = false;
|
||||
for (var i = 0; i < result.length; i++) {
|
||||
$scope.groupsById[result[i].id] = result[i];
|
||||
if (result[i].source === '') $scope.hasLocalGroups = true;
|
||||
}
|
||||
|
||||
if (callback) callback();
|
||||
});
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
function refreshCurrentPage() {
|
||||
refreshGroups(function (error) {
|
||||
if (error) return console.error('Unable to get group listing.', error);
|
||||
refreshUsers(true);
|
||||
refreshUsersCurrentPage(true /* busy indicator */);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.showNextPage = function () {
|
||||
$scope.currentPage++;
|
||||
refreshUsers();
|
||||
refreshUsersCurrentPage(false /* no busy indicator */);
|
||||
};
|
||||
|
||||
$scope.showPrevPage = function () {
|
||||
if ($scope.currentPage > 1) $scope.currentPage--;
|
||||
else $scope.currentPage = 1;
|
||||
refreshUsers();
|
||||
refreshUsersCurrentPage(false /* no busy indicator */);
|
||||
};
|
||||
|
||||
$scope.updateFilter = function () {
|
||||
refreshUsers();
|
||||
refreshUsersCurrentPage(false /* no busy indicator */);
|
||||
};
|
||||
|
||||
function refreshAllUsers() { // this loads all users on Cloudron, not just current page
|
||||
@@ -911,7 +894,7 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
}
|
||||
|
||||
Client.onReady(function () {
|
||||
refresh();
|
||||
refreshCurrentPage();
|
||||
refreshAllUsers();
|
||||
|
||||
// Order matters for permissions used in canEdit
|
||||
|
||||
@@ -95,7 +95,7 @@ angular.module('Application').controller('VolumesController', ['$scope', '$locat
|
||||
ext4Disk: null, // { path, type }
|
||||
xfsDisk: null, // { path, type }
|
||||
user: '',
|
||||
seal: false,
|
||||
seal: true,
|
||||
port: 22,
|
||||
privateKey: '',
|
||||
|
||||
@@ -112,7 +112,7 @@ angular.module('Application').controller('VolumesController', ['$scope', '$locat
|
||||
$scope.volumeAdd.ext4Disk = null;
|
||||
$scope.volumeAdd.xfsDisk = null;
|
||||
$scope.volumeAdd.user = '';
|
||||
$scope.volumeAdd.seal = false;
|
||||
$scope.volumeAdd.seal = true;
|
||||
$scope.volumeAdd.port = 22;
|
||||
$scope.volumeAdd.privateKey = '';
|
||||
|
||||
@@ -213,7 +213,7 @@ angular.module('Application').controller('VolumesController', ['$scope', '$locat
|
||||
username: '',
|
||||
password: '',
|
||||
user: '',
|
||||
seal: false,
|
||||
seal: true,
|
||||
port: 22,
|
||||
privateKey: '',
|
||||
|
||||
@@ -224,7 +224,7 @@ angular.module('Application').controller('VolumesController', ['$scope', '$locat
|
||||
$scope.volumeEdit.name = '';
|
||||
$scope.volumeEdit.mountType = '';
|
||||
$scope.volumeEdit.host = '';
|
||||
$scope.volumeEdit.seal = '';
|
||||
$scope.volumeEdit.seal = true;
|
||||
$scope.volumeEdit.port = '';
|
||||
$scope.volumeEdit.remoteDir = '';
|
||||
$scope.volumeEdit.username = '';
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
const js = require('@eslint/js');
|
||||
const globals = require('globals');
|
||||
|
||||
module.exports = [
|
||||
js.configs.recommended,
|
||||
{
|
||||
files: ["**/*.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
},
|
||||
ecmaVersion: 13,
|
||||
sourceType: "commonjs"
|
||||
},
|
||||
rules: {
|
||||
semi: "error",
|
||||
"prefer-const": "error"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>File Manager</title>
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: black;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Logs</title>
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: black;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
Generated
+640
-1009
File diff suppressed because it is too large
Load Diff
+16
-17
@@ -4,30 +4,29 @@
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --base=/frontend/ --strictPort --port 4001",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/noto-sans": "^5.0.12",
|
||||
"@fontsource/noto-sans": "^5.0.22",
|
||||
"@xterm/addon-attach": "^0.11.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"anser": "^2.1.1",
|
||||
"combokeys": "^3.0.1",
|
||||
"filesize": "^10.0.12",
|
||||
"marked": "^7.0.4",
|
||||
"moment": "^2.29.4",
|
||||
"pankow": "^1.0.1",
|
||||
"primeicons": "^6.0.1",
|
||||
"primevue": "^3.34.1",
|
||||
"superagent": "^8.1.2",
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "^9.4.1",
|
||||
"vue-router": "^4.2.4",
|
||||
"xterm": "^5.2.1",
|
||||
"xterm-addon-attach": "^0.8.0",
|
||||
"xterm-addon-fit": "^0.7.0"
|
||||
"filesize": "^10.1.4",
|
||||
"marked": "^13.0.2",
|
||||
"moment": "^2.30.1",
|
||||
"pankow": "^1.6.8",
|
||||
"pankow-viewers": "^1.0.4",
|
||||
"superagent": "^9.0.2",
|
||||
"vue": "^3.4.33",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.3.4",
|
||||
"vite": "^4.4.9"
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"vite": "^5.3.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
<template>
|
||||
<ConfirmDialog/>
|
||||
<!-- router-view needs some fake node first for some unknown reason -->
|
||||
<span></span>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import ConfirmDialog from 'primevue/confirmdialog';
|
||||
|
||||
export default {
|
||||
components: { ConfirmDialog },
|
||||
data() {
|
||||
return {
|
||||
};
|
||||
|
||||
@@ -8,32 +8,25 @@
|
||||
<span class="title">{{ name }}</span>
|
||||
</template>
|
||||
<template #right>
|
||||
<Button type="button" :label="$t('logs.clear')" icon="pi pi-eraser" @click="onClear()" style="margin-right: 5px" />
|
||||
<a :href="downloadUrl" target="_blank"><Button :label="$t('logs.download')" icon="pi pi-download" /></a>
|
||||
<a class="hide-phone" style="margin-left: 5px; margin-right: 5px;" :href="'/frontend/filemanager.html#/home/app/' + id" target="_blank" v-show="type === 'app'"><Button type="button" severity="secondary" icon="pi pi-folder" :label="$t('filemanager.title')" /></a>
|
||||
<a class="hide-phone" style="margin-right: 5px;" :href="'/frontend/terminal.html?id=' + id" target="_blank" v-show="type === 'app'"><Button severity="secondary" icon="pi pi-desktop" :label="$t('terminal.title')" /></a>
|
||||
<Button class="hide-phone" type="button" :label="$t('filemanager.toolbar.restartApp')" severity="secondary" icon="pi pi-sync" @click="onRestartApp" :loading="busyRestart" v-show="type === 'app'"/>
|
||||
<Button icon="fa-solid fa-eraser" @click="onClear()" style="margin-right: 5px">{{ $t('logs.clear') }}</Button>
|
||||
<Button :href="downloadUrl" target="_blank" icon="fa-solid fa-download">{{ $t('logs.download') }}</Button>
|
||||
|
||||
<Button style="margin-left: 20px;" :title="$t('filemanager.toolbar.restartApp')" v-show="showRestart" secondary :loading="busyRestart" icon="fa-solid fa-arrows-rotate" @click="onRestartApp"/>
|
||||
<Button :href="'/frontend/terminal.html?id=' + id" target="_blank" v-show="showTerminal" secondary icon="fa-solid fa-terminal" :title="$t('terminal.title')" />
|
||||
<Button :href="'/frontend/filemanager.html#/home/app/' + id" target="_blank" v-show="showFilemanager" secondary icon="fa-solid fa-folder" :title="$t('filemanager.title')" />
|
||||
</template>
|
||||
</TopBar>
|
||||
</template>
|
||||
<template #body>
|
||||
<div v-for="line in logLines" :key="line.id" class="log-line">
|
||||
<span class="time">{{ line.time }}</span><span v-html="line.html"></span>
|
||||
</div>
|
||||
<div ref="scrollAnchor" class="bottom-spacer"></div>
|
||||
<div ref="linesContainer"></div>
|
||||
<div class="bottom-spacer"></div>
|
||||
</template>
|
||||
</MainLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Button from 'primevue/button';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Menu from 'primevue/menu';
|
||||
import ProgressSpinner from 'primevue/progressspinner';
|
||||
|
||||
import { TopBar, MainLayout } from 'pankow';
|
||||
import { Button, TopBar, MainLayout } from 'pankow';
|
||||
|
||||
import LogsModel from '../models/LogsModel.js';
|
||||
import AppModel from '../models/AppModel.js';
|
||||
@@ -44,11 +37,7 @@ export default {
|
||||
name: 'LogsViewer',
|
||||
components: {
|
||||
Button,
|
||||
Dialog,
|
||||
InputText,
|
||||
MainLayout,
|
||||
Menu,
|
||||
ProgressSpinner,
|
||||
TopBar
|
||||
},
|
||||
data() {
|
||||
@@ -58,6 +47,9 @@ export default {
|
||||
logsModel: null,
|
||||
appModel: null,
|
||||
busyRestart: false,
|
||||
showRestart: false,
|
||||
showFilemanager: false,
|
||||
showTerminal: false,
|
||||
id: '',
|
||||
name: '',
|
||||
type: '',
|
||||
@@ -125,9 +117,13 @@ export default {
|
||||
|
||||
if (this.type === 'app') {
|
||||
this.appModel = AppModel.create(this.apiOrigin, this.accessToken, this.id);
|
||||
|
||||
try {
|
||||
const app = await this.appModel.get();
|
||||
this.name = `${app.label || app.fqdn} (${app.manifest.title})`;
|
||||
this.showFilemanager = !!app.manifest.addons.localstorage;
|
||||
this.showTerminal = app.manifest.id !== 'io.cloudron.builtin.appproxy';
|
||||
this.showRestart = app.manifest.id !== 'io.cloudron.builtin.appproxy';
|
||||
} catch (e) {
|
||||
console.error(`Failed to get app info for ${this.id}:`, e);
|
||||
}
|
||||
@@ -137,14 +133,33 @@ export default {
|
||||
|
||||
this.downloadUrl = this.logsModel.getDownloadUrl();
|
||||
|
||||
this.logsModel.stream((id, time, html) => {
|
||||
this.logLines.push({ id, time, html});
|
||||
const maxLines = 1000;
|
||||
let lines = 0;
|
||||
let newLogLines = [];
|
||||
|
||||
const tmp = document.getElementsByClassName('cloudron-layout-body')[0];
|
||||
if (!tmp) return;
|
||||
const tmp = document.getElementsByClassName('pankow-main-layout-body')[0];
|
||||
setInterval(() => {
|
||||
newLogLines = newLogLines.slice(-maxLines)
|
||||
|
||||
const autoScroll = tmp.scrollTop > (tmp.scrollHeight - tmp.clientHeight - 34);
|
||||
if (autoScroll) setTimeout(() => this.$refs.scrollAnchor.scrollIntoView(false), 1);
|
||||
for (let line of newLogLines) {
|
||||
if (lines < maxLines) ++lines;
|
||||
else this.$refs.linesContainer.removeChild(this.$refs.linesContainer.firstChild);
|
||||
|
||||
// this.logLines.push({ time, html});
|
||||
const logLine = document.createElement('div');
|
||||
logLine.className = 'log-line';
|
||||
logLine.innerHTML = `<span class="time">${line.time || '[no timestamp] ' }</span> <span>${line.html}</span>`;
|
||||
this.$refs.linesContainer.appendChild(logLine);
|
||||
|
||||
const autoScroll = tmp.scrollTop > (tmp.scrollHeight - tmp.clientHeight - 34);
|
||||
if (autoScroll) setTimeout(() => tmp.scrollTop = tmp.scrollHeight, 1);
|
||||
}
|
||||
|
||||
newLogLines = [];
|
||||
}, 500);
|
||||
|
||||
this.logsModel.stream((time, html) => {
|
||||
newLogLines.push({ time, html });
|
||||
}, function (error) {
|
||||
console.error('Failed to start log stream:', error);
|
||||
})
|
||||
@@ -163,7 +178,7 @@ body {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.cloudron-layout-body {
|
||||
.pankow-main-layout-body {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
@@ -173,7 +188,7 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.cloudron-top {
|
||||
.pankow-top-bar {
|
||||
background-color: black;
|
||||
color: white;
|
||||
margin-bottom: 0 !important;
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
<template>
|
||||
<MainLayout :gap="false">
|
||||
<template #dialogs>
|
||||
<Dialog v-model:visible="fatalError" modal header="Error" :closable="false" :closeOnEscape="false">
|
||||
<Dialog ref="fatalErrorDialog" modal title="Error">
|
||||
<p>{{ fatalError }}</p>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="downloadFileDialog.visible" modal :style="{ width: '50vw' }" @show="onDialogShow('downloadFileDialogNameInput')">
|
||||
<template #header>
|
||||
<label class="dialog-header" for="downloadFileDialogNameInput">{{ $t('terminal.downloadAction') }}</label>
|
||||
</template>
|
||||
<template #default>
|
||||
<form @submit="onDownloadFileDialogSubmit" @submit.prevent>
|
||||
<p :v-show="downloadFileDialog.error">{{ downloadFileDialog.error }}</p>
|
||||
<label for="downloadFileDialogNameInput">{{ $t('terminal.download.filePath') }}</label>
|
||||
<InputText class="dialog-single-input" :class="{ 'p-invalid': downloadFileDialog.error }" id="downloadFileDialogNameInput" v-model="downloadFileDialog.name" :disabled="downloadFileDialog.busy" required/>
|
||||
<Button class="dialog-single-input-submit" type="submit" :label="$t('terminal.download.download')" :loading="downloadFileDialog.busy" :disabled="downloadFileDialog.busy || !downloadFileDialog.name"/>
|
||||
</form>
|
||||
<a id="fileDownloadLink" :href="downloadFileDialog.downloadUrl" target="_blank"></a>
|
||||
</template>
|
||||
</Dialog>
|
||||
<InputDialog ref="inputDialog" />
|
||||
<a id="fileDownloadLink" :href="downloadFileDownloadUrl" target="_blank"></a>
|
||||
</template>
|
||||
<template #header>
|
||||
<TopBar class="navbar">
|
||||
@@ -27,22 +15,21 @@
|
||||
</template>
|
||||
<template #right>
|
||||
<!-- Scheduler/cron tasks -->
|
||||
<Button severity="success" :label="$t('terminal.scheduler')" v-show="usesAddon('scheduler')" icon="pi pi-angle-down" iconPos="right" @click="onSchedulerMenu" aria-haspopup="true" aria-controls="schedulerMenu" style="margin-right: 5px" />
|
||||
<Menu ref="schedulerMenu" id="schedulerMenu" :model="schedulerMenuModel" :popup="true" />
|
||||
<Button success :menu="schedulerMenuModel" v-show="usesAddon('scheduler')" @click="onSchedulerMenu">{{ $t('terminal.scheduler') }}</Button>
|
||||
|
||||
<!-- addon actions -->
|
||||
<Button severity="success" style="margin-right: 5px;" @click="terminalInject('mysql')" v-show="usesAddon('mysql')" :disabled="!connected" label="MySQL"/>
|
||||
<Button severity="success" style="margin-right: 5px;" @click="terminalInject('postgresql')" v-show="usesAddon('postgresql')" :disabled="!connected" label="Postgres"/>
|
||||
<Button severity="success" style="margin-right: 5px;" @click="terminalInject('mongodb')" v-show="usesAddon('mongodb')" :disabled="!connected" label="MongoDB"/>
|
||||
<Button severity="success" style="margin-right: 5px;" @click="terminalInject('redis')" v-show="usesAddon('redis')" :disabled="!connected" label="Redis"/>
|
||||
<Button success @click="terminalInject('mysql')" v-show="usesAddon('mysql')" :disabled="!connected">MySQL</Button>
|
||||
<Button success @click="terminalInject('postgresql')" v-show="usesAddon('postgresql')" :disabled="!connected">Postgres</Button>
|
||||
<Button success @click="terminalInject('mongodb')" v-show="usesAddon('mongodb')" :disabled="!connected">MongoDB</Button>
|
||||
<Button success @click="terminalInject('redis')" v-show="usesAddon('redis')" :disabled="!connected">Redis</Button>
|
||||
|
||||
<!-- upload/download actions -->
|
||||
<Button severity="primary" style="margin-right: 5px;" :disabled="!connected" @click="onUpload" icon="pi pi-upload" :label="$t('terminal.uploadToTmp')"/>
|
||||
<Button severity="primary" style="margin-right: 5px;" :disabled="!connected" @click="onDownload" icon="pi pi-download" :label="$t('terminal.downloadAction')"/>
|
||||
<Button style="margin-left: 20px;" :disabled="!connected" @click="onUpload" icon="fa-solid fa-upload">{{ $t('terminal.uploadTo', { path: '/app/data/' }) }}</Button>
|
||||
<Button :disabled="!connected" @click="onDownload" icon="fa-solid fa-download">{{ $t('terminal.downloadAction') }}</Button>
|
||||
|
||||
<Button style="margin-right: 5px;" severity="secondary" type="button" v-tooltip.bottom="$t('filemanager.toolbar.restartApp')" icon="pi pi-sync" @click="onRestartApp" :loading="busyRestart"/>
|
||||
<a style="margin-right: 5px;" :href="'/frontend/filemanager.html#/home/app/' + id" target="_blank"><Button severity="secondary" type="button" icon="pi pi-folder" v-tooltip.bottom="$t('filemanager.title')" /></a>
|
||||
<a :href="'/frontend/logs.html?appId=' + id" target="_blank"><Button severity="secondary" icon="pi pi-align-left" v-tooltip.bottom="$t('logs.title')"/></a>
|
||||
<Button style="margin-left: 20px;" :title="$t('filemanager.toolbar.restartApp')" secondary :loading="busyRestart" icon="fa-solid fa-arrows-rotate" @click="onRestartApp"/>
|
||||
<Button v-show="showFilemanager" :href="'/frontend/filemanager.html#/home/app/' + id" target="_blank" secondary icon="fa-solid fa-folder" :title="$t('filemanager.title')" />
|
||||
<Button :href="'/frontend/logs.html?appId=' + id" target="_blank" secondary icon="fa-solid fa-align-left" :title="$t('logs.title')" />
|
||||
</template>
|
||||
</TopBar>
|
||||
</template>
|
||||
@@ -63,20 +50,15 @@
|
||||
|
||||
import superagent from 'superagent';
|
||||
|
||||
import Button from 'primevue/button';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Menu from 'primevue/menu';
|
||||
import ProgressSpinner from 'primevue/progressspinner';
|
||||
import { Button, Dialog, FileUploader, InputDialog, MainLayout, TopBar } from 'pankow';
|
||||
|
||||
import { TopBar, MainLayout, FileUploader } from 'pankow';
|
||||
|
||||
import 'xterm/css/xterm.css';
|
||||
import { Terminal } from 'xterm';
|
||||
import { AttachAddon } from 'xterm-addon-attach';
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
import '@xterm/xterm/css/xterm.css';
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { AttachAddon } from '@xterm/addon-attach';
|
||||
import { FitAddon } from '@xterm/addon-fit';
|
||||
|
||||
import { create } from '../models/AppModel.js';
|
||||
import { createDirectoryModel } from '../models/DirectoryModel.js';
|
||||
|
||||
const API_ORIGIN = import.meta.env.VITE_API_ORIGIN ? 'https://' + import.meta.env.VITE_API_ORIGIN : window.location.origin;
|
||||
|
||||
@@ -86,10 +68,8 @@ export default {
|
||||
Button,
|
||||
Dialog,
|
||||
FileUploader,
|
||||
InputText,
|
||||
InputDialog,
|
||||
MainLayout,
|
||||
Menu,
|
||||
ProgressSpinner,
|
||||
TopBar
|
||||
},
|
||||
data() {
|
||||
@@ -97,10 +77,12 @@ export default {
|
||||
accessToken: localStorage.token,
|
||||
apiOrigin: API_ORIGIN || '',
|
||||
appModel: null,
|
||||
directoryModel: null,
|
||||
fatalError: false,
|
||||
busyRestart: false,
|
||||
connected: false,
|
||||
addons: {},
|
||||
showFilemanager: false,
|
||||
schedulerTasks: [],
|
||||
manifestVersion: '',
|
||||
schedulerMenuModel: [],
|
||||
@@ -108,61 +90,57 @@ export default {
|
||||
name: '',
|
||||
socket: null,
|
||||
terminal: null,
|
||||
downloadFileDialog: {
|
||||
busy: false,
|
||||
name: '',
|
||||
error: '',
|
||||
downloadUrl: '',
|
||||
visible: false,
|
||||
}
|
||||
downloadFileDownloadUrl: ''
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onFatalError(errorMessage) {
|
||||
this.fatalError = errorMessage;
|
||||
this.$refs.fatalErrorDialog.open();
|
||||
},
|
||||
// generic dialog focus handler TODO move to pankow and reuse in filemanger
|
||||
onDialogShow(focusElementId) {
|
||||
setTimeout(() => document.getElementById(focusElementId).focus(), 0);
|
||||
},
|
||||
onDownload() {
|
||||
this.downloadFileDialog.busy = false;
|
||||
this.downloadFileDialog.name = '';
|
||||
this.downloadFileDialog.error = '';
|
||||
this.downloadFileDialog.downloadUrl = '';
|
||||
this.downloadFileDialog.visible = true;
|
||||
},
|
||||
async onDownloadFileDialogSubmit() {
|
||||
this.downloadFileDialog.busy = true;
|
||||
async onDownload() {
|
||||
this.downloadFileDownloadUrl = '';
|
||||
|
||||
const downloadFileName = await this.$refs.inputDialog.prompt({
|
||||
message: this.$t('terminal.downloadAction'),
|
||||
value: '',
|
||||
confirmStyle: 'success',
|
||||
confirmLabel: this.$t('terminal.download.download'),
|
||||
rejectLabel: this.$t('main.dialog.cancel'),
|
||||
modal: false
|
||||
});
|
||||
|
||||
if (!downloadFileName) return;
|
||||
|
||||
try {
|
||||
const result = await superagent.head(`${this.apiOrigin}/api/v1/apps/${this.id}/download`).query({
|
||||
file: this.downloadFileDialog.name,
|
||||
file: downloadFileName,
|
||||
access_token: this.accessToken
|
||||
});
|
||||
} catch (error) {
|
||||
this.downloadFileDialog.busy = false;
|
||||
|
||||
if (error.status === 404) this.downloadFileDialog.error = 'The requested file does not exist.';
|
||||
if (error.status === 404) console.error('The requested file does not exist.');
|
||||
else console.error('Failed', error);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.downloadFileDialog.downloadUrl = `${this.apiOrigin}/api/v1/apps/${this.id}/download?file=${encodeURIComponent(this.downloadFileDialog.name)}&access_token=${this.accessToken}`;
|
||||
this.downloadFileDownloadUrl = `${this.apiOrigin}/api/v1/apps/${this.id}/download?file=${encodeURIComponent(downloadFileName)}&access_token=${this.accessToken}`;
|
||||
|
||||
// we have to click the link to make the browser do the download
|
||||
// don't know how to prevent the browsers
|
||||
this.$nextTick(() => {
|
||||
document.getElementById('fileDownloadLink').click();
|
||||
this.downloadFileDialog.visible = false;
|
||||
});
|
||||
},
|
||||
onUpload() {
|
||||
this.$refs.fileUploader.onUploadFile('/tmp');
|
||||
this.$refs.fileUploader.onUploadFile('/');
|
||||
},
|
||||
async uploadHandler(targetDir, file, progressHandler) {
|
||||
await superagent.post(`${this.apiOrigin}/api/v1/apps/${this.id}/upload`)
|
||||
.query({ access_token: this.accessToken, file: `${targetDir}/${file.name}` })
|
||||
.attach('file', file)
|
||||
.on('progress', progressHandler);
|
||||
await this.directoryModel.upload(targetDir, file, progressHandler);
|
||||
},
|
||||
usesAddon(addon) {
|
||||
return !!Object.keys(this.addons).find(function (a) { return a === addon; });
|
||||
@@ -192,6 +170,8 @@ export default {
|
||||
} else if (addon === 'redis') {
|
||||
if (this.manifestVersion === 1) {
|
||||
cmd = 'redis-cli -h "${REDIS_HOST}" -p "${REDIS_PORT}" -a "${REDIS_PASSWORD}"';
|
||||
} else if (this.addons['redis'].noPassword) {
|
||||
cmd = 'redis-cli -h "${CLOUDRON_REDIS_HOST}" -p "${CLOUDRON_REDIS_PORT}"';
|
||||
} else {
|
||||
cmd = 'redis-cli -h "${CLOUDRON_REDIS_HOST}" -p "${CLOUDRON_REDIS_PORT}" -a "${CLOUDRON_REDIS_PASSWORD}" --no-auth-warning';
|
||||
}
|
||||
@@ -215,7 +195,7 @@ export default {
|
||||
this.busyRestart = false;
|
||||
},
|
||||
async connect(retry = false) {
|
||||
document.getElementsByClassName('cloudron-layout-body')[0].innerHTML = '';
|
||||
document.getElementsByClassName('pankow-main-layout-body')[0].innerHTML = '';
|
||||
|
||||
let execId;
|
||||
try {
|
||||
@@ -227,7 +207,7 @@ export default {
|
||||
}
|
||||
|
||||
this.terminal = new Terminal();
|
||||
this.terminal.open(document.getElementsByClassName('cloudron-layout-body')[0]);
|
||||
this.terminal.open(document.getElementsByClassName('pankow-main-layout-body')[0]);
|
||||
|
||||
if (retry) this.terminal.writeln('Reconnecting...');
|
||||
else this.terminal.writeln('Connecting...');
|
||||
@@ -247,11 +227,6 @@ export default {
|
||||
|
||||
this.terminal.loadAddon(new AttachAddon(this.socket));
|
||||
|
||||
// Let the browser handle paste
|
||||
// this.terminal.attachCustomKeyEventHandler((event) => {
|
||||
// if (event.key === 'V' && (event.ctrlKey || event.metaKey)) return false;
|
||||
// });
|
||||
|
||||
this.socket.addEventListener('open', (event) => {
|
||||
this.connected = true;
|
||||
});
|
||||
@@ -289,27 +264,34 @@ export default {
|
||||
this.name = id;
|
||||
|
||||
this.appModel = create(this.apiOrigin, this.accessToken, this.id);
|
||||
this.directoryModel = createDirectoryModel(this.apiOrigin, this.accessToken, `apps/${id}`);
|
||||
|
||||
try {
|
||||
const app = await this.appModel.get();
|
||||
this.name = `${app.label || app.fqdn} (${app.manifest.title})`;
|
||||
this.addons = app.manifest.addons;
|
||||
this.manifestVersion = app.manifest.manifestVersion;
|
||||
this.showFilemanager = !!app.manifest.addons.localstorage;
|
||||
|
||||
this.schedulerMenuModel = !app.manifest.addons.scheduler ? [] : Object.keys(app.manifest.addons.scheduler).map((k) => {
|
||||
return {
|
||||
label: () => k,
|
||||
command: () => this.terminalInject('scheduler', app.manifest.addons.scheduler[k].command)
|
||||
label: k,
|
||||
action: () => this.terminalInject('scheduler', app.manifest.addons.scheduler[k].command)
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(`Failed to get app info for ${this.id}:`, e);
|
||||
this.fatalError = `Unknown app ${this.id}. Cannot continue.`;
|
||||
return;
|
||||
return this.onFatalError(`Unknown app ${this.id}. Cannot continue.`);
|
||||
}
|
||||
|
||||
window.document.title = `Terminal - ${this.name}`;
|
||||
|
||||
window.addEventListener('beforeunload', function (e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}, true );
|
||||
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'C' && (event.ctrlKey || event.metaKey)) { // ctrl shift c
|
||||
event.preventDefault();
|
||||
@@ -335,13 +317,14 @@ export default {
|
||||
|
||||
body {
|
||||
background-color: black;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.cloudron-top {
|
||||
.pankow-top-bar {
|
||||
background-color: black;
|
||||
color: white;
|
||||
margin-bottom: 0 !important;
|
||||
|
||||
+12
-14
@@ -5,15 +5,7 @@ import './style.css';
|
||||
|
||||
import '@fontsource/noto-sans';
|
||||
|
||||
import 'primevue/resources/themes/saga-blue/theme.css';
|
||||
// import 'primevue/resources/themes/arya-blue/theme.css';
|
||||
import 'primevue/resources/primevue.min.css';
|
||||
import 'primeicons/primeicons.css';
|
||||
|
||||
import PrimeVue from 'primevue/config';
|
||||
import ConfirmationService from 'primevue/confirmationservice';
|
||||
import superagent from 'superagent';
|
||||
import Tooltip from 'primevue/tooltip';
|
||||
|
||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||
|
||||
@@ -35,9 +27,18 @@ const router = createRouter({
|
||||
|
||||
const translations = {};
|
||||
const i18n = createI18n({
|
||||
locale: 'en', // set locale
|
||||
fallbackLocale: 'en', // set fallback locale
|
||||
messages: translations
|
||||
locale: 'en', // set locale
|
||||
fallbackLocale: 'en', // set fallback locale
|
||||
messages: translations,
|
||||
// will replace our double {{}} to vue-i18n single brackets
|
||||
messageResolver: (keys, key) => {
|
||||
let message = key.split('.').reduce((o, k) => o && o[k] || null, keys);
|
||||
|
||||
// fallback tr key
|
||||
if (message === null) message = key;
|
||||
|
||||
return message.replaceAll('{{', '{').replaceAll('}}', '}');
|
||||
}
|
||||
});
|
||||
|
||||
// https://vue-i18n.intlify.dev/guide/advanced/lazy.html
|
||||
@@ -72,9 +73,6 @@ const i18n = createI18n({
|
||||
|
||||
app.use(i18n);
|
||||
app.use(router);
|
||||
app.use(PrimeVue, { ripple: true });
|
||||
app.directive('tooltip', Tooltip);
|
||||
app.use(ConfirmationService);
|
||||
|
||||
app.mount('#app');
|
||||
})();
|
||||
|
||||
+12
-12
@@ -5,22 +5,24 @@ import './style.css';
|
||||
|
||||
import '@fontsource/noto-sans';
|
||||
|
||||
import 'primevue/resources/themes/saga-blue/theme.css';
|
||||
// import 'primevue/resources/themes/arya-blue/theme.css';
|
||||
import 'primevue/resources/primevue.min.css';
|
||||
import 'primeicons/primeicons.css';
|
||||
|
||||
import PrimeVue from 'primevue/config';
|
||||
import ConfirmationService from 'primevue/confirmationservice';
|
||||
import superagent from 'superagent';
|
||||
|
||||
import LogsViewer from './components/LogsViewer.vue';
|
||||
|
||||
const translations = {};
|
||||
const i18n = createI18n({
|
||||
locale: 'en', // set locale
|
||||
fallbackLocale: 'en', // set fallback locale
|
||||
messages: translations
|
||||
locale: 'en', // set locale
|
||||
fallbackLocale: 'en', // set fallback locale
|
||||
messages: translations,
|
||||
// will replace our double {{}} to vue-i18n single brackets
|
||||
messageResolver: (keys, key) => {
|
||||
let message = key.split('.').reduce((o, k) => o && o[k] || null, keys);
|
||||
|
||||
// fallback tr key
|
||||
if (message === null) message = key;
|
||||
|
||||
return message.replaceAll('{{', '{').replaceAll('}}', '}');
|
||||
}
|
||||
});
|
||||
|
||||
// https://vue-i18n.intlify.dev/guide/advanced/lazy.html
|
||||
@@ -54,8 +56,6 @@ const i18n = createI18n({
|
||||
const app = createApp(LogsViewer);
|
||||
|
||||
app.use(i18n);
|
||||
app.use(PrimeVue, { ripple: true });
|
||||
app.use(ConfirmationService);
|
||||
|
||||
app.mount('#app');
|
||||
})();
|
||||
|
||||
@@ -5,9 +5,26 @@ import { sanitize } from 'pankow/utils';
|
||||
const BASE_URL = import.meta.env.BASE_URL || '/';
|
||||
|
||||
export function createDirectoryModel(origin, accessToken, api) {
|
||||
const ownersModel = [{
|
||||
uid: 0,
|
||||
label: 'root'
|
||||
}, {
|
||||
uid: 33,
|
||||
label: 'www-data'
|
||||
}, {
|
||||
uid: 808,
|
||||
label: 'yellowtent'
|
||||
}, {
|
||||
uid: 1000,
|
||||
label: 'cloudron'
|
||||
}, {
|
||||
uid: 1001,
|
||||
label: 'git'
|
||||
}];
|
||||
|
||||
return {
|
||||
name: 'DirectoryModel',
|
||||
ownersModel,
|
||||
buildFilePath(filePath, fileName) {
|
||||
// remove leading and trailing slashes
|
||||
while (filePath.startsWith('/')) filePath = filePath.slice(1);
|
||||
@@ -45,10 +62,7 @@ export function createDirectoryModel(origin, accessToken, api) {
|
||||
}
|
||||
|
||||
item.owner = item.uid;
|
||||
if (item.uid === 0) item.owner = 'root';
|
||||
if (item.uid === 33) item.owner = 'www-data';
|
||||
if (item.uid === 1000) item.owner = 'cloudron';
|
||||
if (item.uid === 1001) item.owner = 'git';
|
||||
if (ownersModel.find((m) => m.uid === item.uid)) item.owner = ownersModel.find((m) => m.uid === item.uid).label;
|
||||
});
|
||||
|
||||
return result.body.entries;
|
||||
@@ -57,10 +71,38 @@ export function createDirectoryModel(origin, accessToken, api) {
|
||||
// file may contain a file name or a file path + file name
|
||||
const relativefilePath = (file.webkitRelativePath ? file.webkitRelativePath : file.name);
|
||||
|
||||
await superagent.post(`${origin}/api/v1/${api}/files/${encodeURIComponent(sanitize(targetDir + '/' + relativefilePath))}`)
|
||||
.query({ access_token: accessToken })
|
||||
.attach('file', file)
|
||||
.on('progress', progressHandler);
|
||||
const req = new Promise(function (resolve, reject) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
resolve(xhr.response);
|
||||
} else {
|
||||
reject({
|
||||
status: xhr.status,
|
||||
statusText: xhr.statusText
|
||||
});
|
||||
}
|
||||
});
|
||||
xhr.addEventListener('error', () => {
|
||||
reject({
|
||||
status: xhr.status,
|
||||
statusText: xhr.statusText
|
||||
});
|
||||
});
|
||||
xhr.upload.addEventListener('progress', (event) => {
|
||||
if (event.loaded) progressHandler({ direction: 'upload', loaded: event.loaded});
|
||||
});
|
||||
|
||||
xhr.open('POST', `${origin}/api/v1/${api}/files/${encodeURIComponent(sanitize(targetDir + '/' + relativefilePath))}?access_token=${accessToken}`);
|
||||
|
||||
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
|
||||
xhr.setRequestHeader('Content-Length', file.size);
|
||||
|
||||
xhr.send(file);
|
||||
});
|
||||
|
||||
const res = await req;
|
||||
},
|
||||
async newFile(folderPath, fileName) {
|
||||
await superagent.post(`${origin}/api/v1/${api}/files/${folderPath}`)
|
||||
@@ -68,17 +110,15 @@ export function createDirectoryModel(origin, accessToken, api) {
|
||||
.attach('file', new File([], fileName));
|
||||
},
|
||||
async newFolder(folderPath) {
|
||||
await superagent.post(`${origin}/api/v1/${api}/files/${folderPath}`)
|
||||
.query({ access_token: accessToken })
|
||||
.send({ directory: true });
|
||||
await superagent.post(`${origin}/api/v1/${api}/files/${folderPath}?access_token=${accessToken}&directory=true`);
|
||||
},
|
||||
async remove(filePath) {
|
||||
await superagent.del(`${origin}/api/v1/${api}/files/${filePath}`)
|
||||
.query({ access_token: accessToken });
|
||||
},
|
||||
async rename(fromFilePath, toFilePath) {
|
||||
async rename(fromFilePath, toFilePath, overwrite = false) {
|
||||
await superagent.put(`${origin}/api/v1/${api}/files/${fromFilePath}`)
|
||||
.send({ action: 'rename', newFilePath: sanitize(toFilePath) })
|
||||
.send({ action: 'rename', newFilePath: sanitize(toFilePath), overwrite })
|
||||
.query({ access_token: accessToken });
|
||||
},
|
||||
async copy(fromFilePath, toFilePath) {
|
||||
|
||||
@@ -69,11 +69,10 @@ export function create(origin, accessToken, type, id) {
|
||||
return console.error(e);
|
||||
}
|
||||
|
||||
const id = data.realtimeTimestamp;
|
||||
const time = data.realtimeTimestamp ? moment(data.realtimeTimestamp/1000).format('MMM DD HH:mm:ss') : '';
|
||||
const html = ansiToHtml(escapeHtml(typeof data.message === 'string' ? data.message : ab2str(data.message)));
|
||||
|
||||
lineHandler(id, time, html);
|
||||
lineHandler(time, html);
|
||||
};
|
||||
},
|
||||
getDownloadUrl() {
|
||||
|
||||
+5
-19
@@ -6,6 +6,7 @@ html, body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: #e5e5e5;
|
||||
color: var(--pankow-text-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
@@ -19,7 +20,6 @@ a {
|
||||
|
||||
a:hover, a:focus {
|
||||
color: #0a6ebd;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
@@ -30,22 +30,8 @@ a:hover, a:focus {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.p-button {
|
||||
font-family: Noto Sans !important;
|
||||
}
|
||||
|
||||
.p-button.p-button-success, .p-buttonset.p-button-success > .p-button, .p-splitbutton.p-button-success > .p-button {
|
||||
background: #27ce65 !important;
|
||||
border: 1px solid #27ce65 !important;
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: black;
|
||||
}
|
||||
}
|
||||
|
||||
+12
-15
@@ -5,24 +5,24 @@ import './style.css';
|
||||
|
||||
import '@fontsource/noto-sans';
|
||||
|
||||
import 'primevue/resources/themes/saga-blue/theme.css';
|
||||
// import 'primevue/resources/themes/arya-blue/theme.css';
|
||||
import 'primevue/resources/primevue.min.css';
|
||||
import 'primeicons/primeicons.css';
|
||||
|
||||
|
||||
import PrimeVue from 'primevue/config';
|
||||
import ConfirmationService from 'primevue/confirmationservice';
|
||||
import superagent from 'superagent';
|
||||
import Tooltip from 'primevue/tooltip';
|
||||
|
||||
import Terminal from './components/Terminal.vue';
|
||||
|
||||
const translations = {};
|
||||
const i18n = createI18n({
|
||||
locale: 'en', // set locale
|
||||
fallbackLocale: 'en', // set fallback locale
|
||||
messages: translations
|
||||
locale: 'en', // set locale
|
||||
fallbackLocale: 'en', // set fallback locale
|
||||
messages: translations,
|
||||
// will replace our double {{}} to vue-i18n single brackets
|
||||
messageResolver: (keys, key) => {
|
||||
let message = key.split('.').reduce((o, k) => o && o[k] || null, keys);
|
||||
|
||||
// fallback tr key
|
||||
if (message === null) message = key;
|
||||
|
||||
return message.replaceAll('{{', '{').replaceAll('}}', '}');
|
||||
}
|
||||
});
|
||||
|
||||
// https://vue-i18n.intlify.dev/guide/advanced/lazy.html
|
||||
@@ -56,9 +56,6 @@ const i18n = createI18n({
|
||||
const app = createApp(Terminal);
|
||||
|
||||
app.use(i18n);
|
||||
app.use(PrimeVue, { ripple: true });
|
||||
app.use(ConfirmationService);
|
||||
app.directive('tooltip', Tooltip);
|
||||
|
||||
app.mount('#app');
|
||||
})();
|
||||
|
||||
+173
-195
@@ -1,67 +1,45 @@
|
||||
<template>
|
||||
<MainLayout>
|
||||
<template #dialogs>
|
||||
<Dialog v-model:visible="fatalError" modal header="Error" :closable="false" :closeOnEscape="false">
|
||||
<Dialog ref="fatalErrorDialog" modal title="Error">
|
||||
<p>{{ fatalError }}</p>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="extractInProgress" modal :header="$t('filemanager.extractionInProgress')" :closable="false" :closeOnEscape="false">
|
||||
<Dialog ref="extractInProgressDialog" modal :title="$t('filemanager.extractionInProgress')">
|
||||
<div style="text-align: center;">
|
||||
<ProgressSpinner style="width: 50px; height: 50px"/>
|
||||
<Spinner style="margin: 10px; width: 50px; height: 50px"/>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="pasteInProgress" modal :header="$t('filemanager.pasteInProgress')" :closable="false" :closeOnEscape="false">
|
||||
<Dialog ref="pasteInProgressDialog" modal :title="$t('filemanager.pasteInProgress')">
|
||||
<div style="text-align: center;">
|
||||
<ProgressSpinner style="width: 50px; height: 50px"/>
|
||||
<Spinner style="margin: 10px; width: 50px; height: 50px"/>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="deleteInProgress" modal :header="$t('filemanager.deleteInProgress')" :closable="false" :closeOnEscape="false">
|
||||
<Dialog ref="deleteInProgressDialog" modal :title="$t('filemanager.deleteInProgress')">
|
||||
<div style="text-align: center;">
|
||||
<ProgressSpinner style="width: 50px; height: 50px"/>
|
||||
<Spinner style="margin: 10px; width: 50px; height: 50px"/>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<!-- have to use v-model instead of : bind - https://github.com/primefaces/primevue/issues/815 -->
|
||||
<Dialog v-model:visible="newFileDialog.visible" modal :style="{ width: '50vw' }" @show="onDialogShow('newFileDialogNameInput')">
|
||||
<template #header>
|
||||
<label class="dialog-header" for="newFileDialogNameInput">{{ $t('filemanager.newFileDialog.title') }}</label>
|
||||
</template>
|
||||
<template #default>
|
||||
<form @submit="onNewFileDialogSubmit" @submit.prevent>
|
||||
<InputText class="dialog-single-input" id="newFileDialogNameInput" v-model="newFileDialog.name" :disabled="newFileDialog.busy" required/>
|
||||
<Button class="dialog-single-input-submit" type="submit" :label="$t('filemanager.newFileDialog.create')" :loading="newFileDialog.busy" :disabled="newFileDialog.busy || !newFileDialog.name"/>
|
||||
</form>
|
||||
</template>
|
||||
</Dialog>
|
||||
<Dialog v-model:visible="newFolderDialog.visible" modal :style="{ width: '50vw' }" @show="onDialogShow('newFolderDialogNameInput')">
|
||||
<template #header>
|
||||
<label class="dialog-header" for="newFolderDialogNameInput">{{ $t('filemanager.newDirectoryDialog.title') }}</label>
|
||||
</template>
|
||||
<template #default>
|
||||
<form @submit="onNewFolderDialogSubmit" @submit.prevent>
|
||||
<InputText class="dialog-single-input" id="newFolderDialogNameInput" v-model="newFolderDialog.name" :disabled="newFolderDialog.busy" required/>
|
||||
<Button class="dialog-single-input-submit" type="submit" :label="$t('filemanager.newFileDialog.create')" :loading="newFolderDialog.busy" :disabled="newFolderDialog.busy || !newFolderDialog.name"/>
|
||||
</form>
|
||||
</template>
|
||||
</Dialog>
|
||||
<InputDialog ref="inputDialog" />
|
||||
</template>
|
||||
<template #header>
|
||||
<TopBar class="navbar">
|
||||
<template #left>
|
||||
<Button icon="pi pi-refresh" @click="onRefresh()" text :loading="busyRefresh" style="margin-right: 5px;"/>
|
||||
<PathBreadcrumbs :path="cwd" :activate-handler="onActivateBreadcrumb"/>
|
||||
<Button icon="fa-solid fa-arrow-rotate-right" :loading="busyRefresh" @click="onRefresh()" secondary plain/>
|
||||
<Breadcrumb :home="breadcrumbHomeItem" :items="breadcrumbItems" :activate-handler="onActivateBreadcrumb"/>
|
||||
</template>
|
||||
<template #right>
|
||||
<Button type="button" :label="$t('filemanager.toolbar.new')" icon="pi pi-plus" @click="onCreateMenu" aria-haspopup="true" aria-controls="create_menu" style="margin-right: 5px" />
|
||||
<Menu ref="createMenu" id="create_menu" :model="createMenuModel" :popup="true" />
|
||||
<Button type="button" :label="$t('filemanager.toolbar.upload')" icon="pi pi-upload" @click="onUploadMenu" aria-haspopup="true" aria-controls="upload_menu" style="margin-right: 5px" />
|
||||
<Menu ref="uploadMenu" id="upload_menu" :model="uploadMenuModel" :popup="true" />
|
||||
<Button icon="fa-solid fa-plus" @click="onCreateMenu">{{ $t('filemanager.toolbar.new') }}</Button>
|
||||
<Menu ref="createMenu" :model="createMenuModel"/>
|
||||
<Button icon="fa-solid fa-upload" @click="onUploadMenu">{{ $t('filemanager.toolbar.upload') }}</Button>
|
||||
<Menu ref="uploadMenu" :model="uploadMenuModel"/>
|
||||
|
||||
<Button style="margin-left: 20px; margin-right: 5px;" type="button" v-tooltip.bottom="$t('filemanager.toolbar.restartApp')" severity="secondary" icon="pi pi-sync" @click="onRestartApp" :loading="busyRestart" v-show="resourceType === 'app'"/>
|
||||
<a style="margin-right: 5px;" :href="'/frontend/terminal.html?id=' + resourceId" target="_blank" v-show="resourceType === 'app'"><Button severity="secondary" icon="pi pi-desktop" v-tooltip.bottom="$t('terminal.title')" /></a>
|
||||
<a :href="'/frontend/logs.html?appId=' + resourceId" target="_blank" v-show="resourceType === 'app'"><Button severity="secondary" icon="pi pi-align-left" v-tooltip.bottom="$t('logs.title')" /></a>
|
||||
<Button style="margin-left: 20px;" :title="$t('filemanager.toolbar.restartApp')" secondary :loading="busyRestart" icon="fa-solid fa-arrows-rotate" @click="onRestartApp" v-show="resourceType === 'app'"/>
|
||||
<Button :href="'/frontend/terminal.html?id=' + resourceId" target="_blank" v-show="resourceType === 'app'" secondary icon="fa-solid fa-terminal" :title="$t('terminal.title')" />
|
||||
<Button :href="'/frontend/logs.html?appId=' + resourceId" target="_blank" v-show="resourceType === 'app'" secondary icon="fa-solid fa-align-left" :title="$t('logs.title')" />
|
||||
</template>
|
||||
</TopBar>
|
||||
</template>
|
||||
@@ -69,6 +47,8 @@
|
||||
<div class="main-view">
|
||||
<div class="main-view-col">
|
||||
<DirectoryView
|
||||
class="directory-view"
|
||||
:busy="busy"
|
||||
:show-owner="true"
|
||||
:show-size="true"
|
||||
:show-modified="true"
|
||||
@@ -95,7 +75,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="main-view-col" style="max-width: 300px;">
|
||||
<div class="title-bar">
|
||||
<div class="side-bar-title">
|
||||
<a v-show="appLink" :href="appLink" target="_blank">{{ title }}</a>
|
||||
<span v-show="!appLink">{{ title }}</span>
|
||||
</div>
|
||||
@@ -107,6 +87,7 @@
|
||||
<FileUploader
|
||||
ref="fileUploader"
|
||||
:upload-handler="uploadHandler"
|
||||
:cancel-handler="onCancelUpload"
|
||||
@finished="onUploadFinished"
|
||||
:tr="$t"
|
||||
/>
|
||||
@@ -122,15 +103,8 @@
|
||||
import superagent from 'superagent';
|
||||
import { marked } from 'marked';
|
||||
|
||||
import Button from 'primevue/button';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Menu from 'primevue/menu';
|
||||
import ProgressSpinner from 'primevue/progressspinner';
|
||||
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
|
||||
import { DirectoryView, TopBar, PathBreadcrumbs, BottomBar, MainLayout, FileUploader } from 'pankow';
|
||||
import { Dialog, DirectoryView, TopBar, Breadcrumb, BottomBar, Button, InputDialog, MainLayout, Menu, FileUploader, Spinner } from 'pankow';
|
||||
import Icon from 'pankow/components/Icon.vue';
|
||||
import { sanitize, sleep } from 'pankow/utils';
|
||||
|
||||
import { ISTATES } from '../constants.js';
|
||||
@@ -154,24 +128,23 @@ export default {
|
||||
Dialog,
|
||||
DirectoryView,
|
||||
FileUploader,
|
||||
InputText,
|
||||
InputDialog,
|
||||
MainLayout,
|
||||
Menu,
|
||||
PathBreadcrumbs,
|
||||
Breadcrumb,
|
||||
PreviewPanel,
|
||||
ProgressSpinner,
|
||||
TopBar
|
||||
Spinner,
|
||||
TopBar,
|
||||
Icon
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
busy: true,
|
||||
fallbackIcon: `${BASE_URL}mime-types/none.svg`,
|
||||
cwd: '/',
|
||||
busyRefresh: false,
|
||||
busyRestart: false,
|
||||
fatalError: false,
|
||||
extractInProgress: false,
|
||||
pasteInProgress: false,
|
||||
deleteInProgress: false,
|
||||
footerContent: '',
|
||||
activeItem: null,
|
||||
activeDirectoryItem: {},
|
||||
@@ -188,50 +161,52 @@ export default {
|
||||
resourceType: '',
|
||||
resourceId: '',
|
||||
visible: true,
|
||||
newFileDialog: {
|
||||
visible: false,
|
||||
busy: false,
|
||||
name: ''
|
||||
uploadRequest: null,
|
||||
breadcrumbHomeItem: {
|
||||
label: '/app/data/',
|
||||
action: () => {
|
||||
this.cwd = '/';
|
||||
}
|
||||
},
|
||||
newFolderDialog: {
|
||||
visible: false,
|
||||
busy: false,
|
||||
name: ''
|
||||
},
|
||||
ownersModel: [{
|
||||
uid: 0,
|
||||
label: 'root'
|
||||
}, {
|
||||
uid: 33,
|
||||
label: 'www-data'
|
||||
}, {
|
||||
uid: 1000,
|
||||
label: 'cloudron'
|
||||
}, {
|
||||
uid: 1001,
|
||||
label: 'git'
|
||||
}],
|
||||
ownersModel: [],
|
||||
// contextMenuModel will have activeItem attached if any command() is called
|
||||
createMenuModel: [{
|
||||
label: () => this.$t('filemanager.toolbar.newFile'),
|
||||
icon: 'pi pi-file',
|
||||
command: this.onNewFile
|
||||
label: this.$t('filemanager.toolbar.newFile'),
|
||||
icon: 'fa-solid fa-file-circle-plus',
|
||||
action: this.onNewFile
|
||||
}, {
|
||||
label: () => this.$t('filemanager.toolbar.newFolder'),
|
||||
icon: 'pi pi-folder',
|
||||
command: this.onNewFolder
|
||||
label: this.$t('filemanager.toolbar.newFolder'),
|
||||
icon: 'fa-solid fa-folder-plus',
|
||||
action: this.onNewFolder
|
||||
}],
|
||||
uploadMenuModel: [{
|
||||
label: () => this.$t('filemanager.toolbar.uploadFile'),
|
||||
icon: 'pi pi-file',
|
||||
command: this.onUploadFile
|
||||
label: this.$t('filemanager.toolbar.uploadFile'),
|
||||
icon: 'fa-solid fa-file-arrow-up',
|
||||
action: this.onUploadFile
|
||||
}, {
|
||||
label: () => this.$t('filemanager.toolbar.newFolder'),
|
||||
icon: 'pi pi-folder',
|
||||
command: this.onUploadFolder
|
||||
label: this.$t('filemanager.toolbar.newFolder'),
|
||||
icon: 'fa-regular fa-folder-open',
|
||||
action: this.onUploadFolder
|
||||
}]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
breadcrumbItems() {
|
||||
const parts = this.cwd.split('/').filter((p) => !!p.trim())
|
||||
const crumbs = [];
|
||||
|
||||
parts.forEach((p, i) => {
|
||||
crumbs.push({
|
||||
label: p,
|
||||
action: () => {
|
||||
this.cwd = '/' + parts.slice(0, i+1).join('/');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return crumbs;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
cwd(newCwd, oldCwd) {
|
||||
if (this.resourceType && this.resourceId) this.$router.push(`/home/${this.resourceType}/${this.resourceId}${this.cwd}`);
|
||||
@@ -239,37 +214,53 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onCreateMenu(event) {
|
||||
this.$refs.createMenu.toggle(event);
|
||||
onFatalError(errorMessage) {
|
||||
this.fatalError = errorMessage;
|
||||
this.$refs.fatalErrorDialog.open();
|
||||
},
|
||||
onUploadMenu(event) {
|
||||
this.$refs.uploadMenu.toggle(event);
|
||||
onCreateMenu(event, elem = null) {
|
||||
this.$refs.createMenu.open(event, elem);
|
||||
},
|
||||
onUploadMenu(event, elem = null) {
|
||||
this.$refs.uploadMenu.open(event, elem);
|
||||
},
|
||||
onCancelUpload() {
|
||||
if (!this.uploadRequest) return;
|
||||
this.uploadRequest.abort();
|
||||
},
|
||||
// generic dialog focus handler
|
||||
onDialogShow(focusElementId) {
|
||||
setTimeout(() => document.getElementById(focusElementId).focus(), 0);
|
||||
},
|
||||
onNewFile() {
|
||||
this.newFileDialog.busy = false;
|
||||
this.newFileDialog.name = '';
|
||||
this.newFileDialog.visible = true;
|
||||
},
|
||||
async onNewFileDialogSubmit() {
|
||||
this.newFileDialog.busy = true;
|
||||
await this.directoryModel.newFile(this.directoryModel.buildFilePath(this.cwd, this.newFileDialog.name), this.newFileDialog.name);
|
||||
async onNewFile() {
|
||||
const newFileName = await this.$refs.inputDialog.prompt({
|
||||
message: this.$t('filemanager.newFileDialog.title'),
|
||||
value: '',
|
||||
confirmStyle: 'success',
|
||||
confirmLabel: this.$t('filemanager.newFileDialog.create'),
|
||||
rejectLabel: this.$t('main.dialog.cancel'),
|
||||
modal: false
|
||||
});
|
||||
|
||||
if (!newFileName) return;
|
||||
|
||||
await this.directoryModel.newFile(this.directoryModel.buildFilePath(this.cwd, newFileName), newFileName);
|
||||
await this.loadCwd();
|
||||
this.newFileDialog.visible = false;
|
||||
},
|
||||
onNewFolder() {
|
||||
this.newFolderDialog.busy = false;
|
||||
this.newFolderDialog.name = '';
|
||||
this.newFolderDialog.visible = true;
|
||||
},
|
||||
async onNewFolderDialogSubmit() {
|
||||
this.newFolderDialog.busy = true;
|
||||
await this.directoryModel.newFolder(this.directoryModel.buildFilePath(this.cwd, this.newFolderDialog.name));
|
||||
async onNewFolder() {
|
||||
const newFolderName = await this.$refs.inputDialog.prompt({
|
||||
message: this.$t('filemanager.newDirectoryDialog.title'),
|
||||
value: '',
|
||||
confirmStyle: 'success',
|
||||
confirmLabel: this.$t('filemanager.newFileDialog.create'),
|
||||
rejectLabel: this.$t('main.dialog.cancel'),
|
||||
modal: false
|
||||
});
|
||||
|
||||
if (!newFolderName) return;
|
||||
|
||||
await this.directoryModel.newFolder(this.directoryModel.buildFilePath(this.cwd, newFolderName));
|
||||
await this.loadCwd();
|
||||
this.newFolderDialog.visible = false;
|
||||
},
|
||||
onUploadFile() {
|
||||
this.$refs.fileUploader.onUploadFile(this.cwd);
|
||||
@@ -335,14 +326,14 @@ export default {
|
||||
if (!files.length) return;
|
||||
|
||||
window.addEventListener('beforeunload', beforeUnloadListener, { capture: true });
|
||||
this.pasteInProgress = true;
|
||||
this.$refs.pasteInProgressDialog.open();
|
||||
|
||||
// check ctrl for cut/copy
|
||||
await this.directoryModel.paste(fullTargetFolder, 'cut', files);
|
||||
await this.loadCwd();
|
||||
|
||||
window.removeEventListener('beforeunload', beforeUnloadListener, { capture: true });
|
||||
this.pasteInProgress = false;
|
||||
this.$refs.pasteInProgressDialog.close();
|
||||
}
|
||||
},
|
||||
onItemActivated(item) {
|
||||
@@ -355,8 +346,26 @@ export default {
|
||||
async deleteHandler(files) {
|
||||
if (!files) return;
|
||||
|
||||
function start_and_end(str) {
|
||||
if (str.length > 100) {
|
||||
return str.substr(0, 45) + ' ... ' + str.substr(str.length-45, str.length);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
const confirmed = await this.$refs.inputDialog.confirm({
|
||||
message: this.$t('filemanager.removeDialog.reallyDelete'),
|
||||
// message: start_and_end(files.map((f) => f.name).join(', ')),
|
||||
confirmStyle: 'danger',
|
||||
confirmLabel: this.$t('main.dialog.yes'),
|
||||
rejectLabel: this.$t('main.dialog.no'),
|
||||
modal: false
|
||||
});
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
window.addEventListener('beforeunload', beforeUnloadListener, { capture: true });
|
||||
this.deleteInProgress = true;
|
||||
this.$refs.deleteInProgressDialog.open();
|
||||
|
||||
for (let i in files) {
|
||||
try {
|
||||
@@ -369,11 +378,30 @@ export default {
|
||||
await this.loadCwd();
|
||||
|
||||
window.removeEventListener('beforeunload', beforeUnloadListener, { capture: true });
|
||||
this.deleteInProgress = false;
|
||||
this.$refs.deleteInProgressDialog.close();
|
||||
},
|
||||
async renameHandler(file, newName) {
|
||||
await this.directoryModel.rename(this.directoryModel.buildFilePath(this.cwd, file.name), sanitize(this.cwd + '/' + newName));
|
||||
await this.loadCwd();
|
||||
if (file.name === newName) return;
|
||||
|
||||
try {
|
||||
await this.directoryModel.rename(this.directoryModel.buildFilePath(this.cwd, file.name), sanitize(this.cwd + '/' + newName));
|
||||
await this.loadCwd();
|
||||
} catch (e) {
|
||||
if (e.status === 409) {
|
||||
const confirmed = await this.$refs.inputDialog.confirm({
|
||||
message: this.$t('filemanager.renameDialog.reallyOverwrite'),
|
||||
confirmStyle: 'danger',
|
||||
confirmLabel: this.$t('main.dialog.yes'),
|
||||
rejectLabel: this.$t('main.dialog.no')
|
||||
});
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
await this.directoryModel.rename(this.directoryModel.buildFilePath(this.cwd, file.name), sanitize(this.cwd + '/' + newName), true /* overwrite */);
|
||||
await this.loadCwd();
|
||||
}
|
||||
else console.error(`Failed to rename ${file} to ${newName}`, e);
|
||||
}
|
||||
},
|
||||
async changeOwnerHandler(files, newOwnerUid) {
|
||||
if (!files) return;
|
||||
@@ -406,26 +434,35 @@ export default {
|
||||
const targetPath = (target && target.isDirectory) ? sanitize(this.cwd + '/' + target.fileName) : this.cwd;
|
||||
|
||||
window.addEventListener('beforeunload', beforeUnloadListener, { capture: true });
|
||||
this.pasteInProgress = true;
|
||||
this.$refs.pasteInProgressDialog.open();
|
||||
|
||||
await this.directoryModel.paste(targetPath, this.clipboard.action, this.clipboard.files);
|
||||
this.clipboard = {};
|
||||
await this.loadCwd();
|
||||
|
||||
window.removeEventListener('beforeunload', beforeUnloadListener, { capture: true });
|
||||
this.pasteInProgress = false;
|
||||
this.$refs.pasteInProgressDialog.close();
|
||||
},
|
||||
async downloadHandler(file) {
|
||||
await this.directoryModel.download(this.directoryModel.buildFilePath(this.cwd, file.name));
|
||||
},
|
||||
async extractHandler(file) {
|
||||
this.extractInProgress = true;
|
||||
this.$refs.extractInProgressDialog.open();
|
||||
await this.directoryModel.extract(this.directoryModel.buildFilePath(this.cwd, file.name));
|
||||
await this.loadCwd();
|
||||
this.extractInProgress = false;
|
||||
this.$refs.extractInProgressDialog.close();
|
||||
},
|
||||
async uploadHandler(targetDir, file, progressHandler) {
|
||||
await this.directoryModel.upload(targetDir, file, progressHandler);
|
||||
this.uploadRequest = this.directoryModel.upload(targetDir, file, progressHandler);
|
||||
|
||||
try {
|
||||
await this.uploadRequest;
|
||||
} catch (e) {
|
||||
console.log('Upload cancelled.');
|
||||
}
|
||||
|
||||
this.uploadRequest = null;
|
||||
|
||||
await this.loadCwd();
|
||||
},
|
||||
async loadCwd() {
|
||||
@@ -442,6 +479,8 @@ export default {
|
||||
mimeType: 'inode/directory',
|
||||
icon: `${BASE_URL}mime-types/inode-directory.svg`
|
||||
};
|
||||
|
||||
this.busy = false;
|
||||
},
|
||||
async onRestartApp() {
|
||||
if (this.resourceType !== 'app') return;
|
||||
@@ -477,8 +516,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
useConfirm();
|
||||
|
||||
this.busy = true;
|
||||
const type = this.$route.params.type || 'app';
|
||||
const resourceId = this.$route.params.resourceId;
|
||||
const cwd = this.$route.params.cwd;
|
||||
@@ -493,24 +531,9 @@ export default {
|
||||
|
||||
if (error || result.statusCode !== 200) {
|
||||
console.error(`Invalid resource ${type} ${resourceId}`, error || result.statusCode);
|
||||
this.fatalError = `Invalid resource ${type} ${resourceId}`;
|
||||
return;
|
||||
return this.onFatalError(`Invalid resource ${type} ${resourceId}`);
|
||||
}
|
||||
|
||||
this.ownersModel = [{
|
||||
uid: 0,
|
||||
label: 'root'
|
||||
}, {
|
||||
uid: 33,
|
||||
label: 'www-data'
|
||||
}, {
|
||||
uid: 1000,
|
||||
label: 'cloudron'
|
||||
}, {
|
||||
uid: 1001,
|
||||
label: 'git'
|
||||
}];
|
||||
|
||||
this.appLink = `https://${result.body.fqdn}`;
|
||||
this.title = `${result.body.label || result.body.fqdn} (${result.body.manifest.title})`;
|
||||
} else if (type === 'volume') {
|
||||
@@ -523,28 +546,12 @@ export default {
|
||||
|
||||
if (error || result.statusCode !== 200) {
|
||||
console.error(`Invalid resource ${type} ${resourceId}`, error || result.statusCode);
|
||||
this.fatalError = `Invalid resource ${type} ${resourceId}`;
|
||||
return;
|
||||
return this.onFatalError(`Invalid resource ${type} ${resourceId}`);
|
||||
}
|
||||
|
||||
this.ownersModel = [{
|
||||
uid: 0,
|
||||
label: 'root'
|
||||
}, {
|
||||
uid: 33,
|
||||
label: 'www-data'
|
||||
}, {
|
||||
uid: 808,
|
||||
label: 'yellowtent'
|
||||
}, {
|
||||
uid: 1001,
|
||||
label: 'git'
|
||||
}];
|
||||
|
||||
this.title = result.body.name;
|
||||
} else {
|
||||
this.fatalError = `Unsupported type ${type}`;
|
||||
return;
|
||||
return this.onFatalError(`Unsupported type ${type}`);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -561,13 +568,12 @@ export default {
|
||||
this.resourceId = resourceId;
|
||||
|
||||
this.directoryModel = createDirectoryModel(this.apiOrigin, this.accessToken, type === 'volume' ? `volumes/${resourceId}` : `apps/${resourceId}`);
|
||||
this.ownersModel = this.directoryModel.ownersModel;
|
||||
|
||||
this.loadCwd();
|
||||
|
||||
this.$watch(() => this.$route.params, (toParams, previousParams) => {
|
||||
if (toParams.type !== 'app' && toParams.type !== 'volume') {
|
||||
this.fatalError = `Unknown type ${toParams.type}`;
|
||||
return;
|
||||
}
|
||||
if (toParams.type !== 'app' && toParams.type !== 'volume') return this.onFatalError(`Unknown type ${toParams.type}`);
|
||||
|
||||
if ((toParams.type !== this.resourceType) || (toParams.resourceId !== this.resourceId)) {
|
||||
this.resourceType = toParams.type;
|
||||
@@ -590,44 +596,21 @@ export default {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
padding: 0 10px
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
.side-bar-title {
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
margin-bottom: 20px;
|
||||
color: #607d8b;
|
||||
}
|
||||
|
||||
.title-bar > a {
|
||||
color: #607d8b;
|
||||
}
|
||||
|
||||
.title-bar > a:hover {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.main-view-col {
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.dialog-single-input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.dialog-single-input-submit {
|
||||
margin-top: 5px;
|
||||
.directory-view {
|
||||
background-color: var(--pankow-color-background);
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -638,9 +621,4 @@ export default {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* this is actually calculated and in some situations the z-index would have to be higher but for the moment ok, needs fixing in primvue */
|
||||
.p-dropdown-panel.p-component {
|
||||
z-index: 5001 !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<div class="viewer">
|
||||
<TextEditor ref="textEditor"
|
||||
<TextViewer ref="textEditor"
|
||||
v-show="active === 'textEditor'"
|
||||
:save-handler="saveHandler"
|
||||
@close="onClose"
|
||||
:tr="$t"
|
||||
/>
|
||||
<ImageViewer ref="imageViewer" v-show="active === 'imageViewer'" @close="onClose"/>
|
||||
<ImageViewer ref="imageViewer" v-show="active === 'imageViewer'" @close="onClose" :navigation-handler="imageViewerNavigationHandler"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { TextEditor, ImageViewer } from 'pankow';
|
||||
import { TextViewer, ImageViewer } from 'pankow-viewers';
|
||||
import { createDirectoryModel } from '../models/DirectoryModel.js';
|
||||
import { sanitize } from 'pankow/utils';
|
||||
|
||||
@@ -22,7 +22,7 @@ export default {
|
||||
name: 'Viewer',
|
||||
components: {
|
||||
ImageViewer,
|
||||
TextEditor
|
||||
TextViewer
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -36,6 +36,9 @@ export default {
|
||||
onClose() {
|
||||
location.replace('#/home' + location.hash.slice('#/viewer'.length, location.hash.lastIndexOf('/')+1));
|
||||
},
|
||||
imageViewerNavigationHandler() {
|
||||
// nothing to do
|
||||
},
|
||||
async saveHandler(item, content) {
|
||||
await this.directoryModel.save(this.filePath, content);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Terminal</title>
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: black;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user