Compare commits
1298 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0dfd217a86 | |||
| 3766101122 | |||
| b7dfa318f3 | |||
| 4ab52c6927 | |||
| 41312354a7 | |||
| f2c6d45c1c | |||
| 2d27a92587 | |||
| 8258a8c777 | |||
| dd364733a4 | |||
| 5e76a8ea7b | |||
| 292034c0e2 | |||
| 9481eccfb0 | |||
| 9d8f21f78d | |||
| 9567efeb45 | |||
| d7cb909600 | |||
| 40e84265e7 | |||
| 9665d1de3a | |||
| 5db0ace3ed | |||
| 436a5d726b | |||
| 0dd97a0dc0 | |||
| d08fb224ba | |||
| 6351e0c3fb | |||
| a8de1ca37b | |||
| 42501fa364 | |||
| 0d6c2dc1cf | |||
| 41099c1131 | |||
| 7af69e080f | |||
| 1c32495f22 | |||
| d51d81cdfa | |||
| aa17196120 | |||
| 262e06dc15 | |||
| 9a148ab7f8 | |||
| 2ec4ad934d | |||
| b4bbdda730 | |||
| d0002eb7ca | |||
| a764a8ad4a | |||
| 6552747290 | |||
| 15a4a7071e | |||
| 219764923b | |||
| 8b35d01f68 | |||
| 2afa13bd7c | |||
| 23d34e59b2 | |||
| 2d999eae9c | |||
| 7fc92101d5 | |||
| 12fa9731b8 | |||
| c67a46e2a9 | |||
| 8a36e2c730 | |||
| 7a66a104ad | |||
| 06d60d5aea | |||
| b4335f3d0d | |||
| 0cc46a8dba | |||
| 2a2b509837 | |||
| 886515e444 | |||
| d5640d45f7 | |||
| 27ec200fc0 | |||
| 4fead2411e | |||
| 9ae69bb683 | |||
| f4c9d7324b | |||
| b9a76aa6b8 | |||
| f55c22bdb9 | |||
| fc0e73657f | |||
| 1ca07a4c92 | |||
| 9f1ab59e35 | |||
| 455cf1bf98 | |||
| 29960b8d6b | |||
| f074ed1ec9 | |||
| b741cfbb21 | |||
| 7a6a9cdbb4 | |||
| 0940ef5b54 | |||
| 6c51cd8d7b | |||
| 814809f103 | |||
| 961cce95d7 | |||
| 963af4334d | |||
| 7b8c721a8a | |||
| 09e9dd0938 | |||
| 36b0d4e1bc | |||
| 0af47bba54 | |||
| 9697dd8b4f | |||
| b604311e2a | |||
| 63394a666e | |||
| fd9efe3da3 | |||
| 9109c89d8f | |||
| 2085a4a7d4 | |||
| e0b6ce9bd8 | |||
| 82f6359547 | |||
| 5dd318b5ab | |||
| 7082dfd418 | |||
| 38211e719e | |||
| fd545a43a6 | |||
| 3a96bdd40a | |||
| df85a70ccc | |||
| 1ed91f40ab | |||
| 9ed19c8b8e | |||
| 93982bae7b | |||
| 5e046a26e9 | |||
| 6b009016b8 | |||
| 8bb4e947a0 | |||
| 346dc4f861 | |||
| ccc5f5f004 | |||
| ac1fd54cce | |||
| 1180820b6f | |||
| 7ea495c361 | |||
| b241c82eba | |||
| 128e3c41a3 | |||
| c25afaa94f | |||
| f35abe1ea0 | |||
| e33a1ca47d | |||
| 765422ac38 | |||
| 0d3e9e32f0 | |||
| bca91d4928 | |||
| 5d7ac82a69 | |||
| 41587ec540 | |||
| 5307a187d5 | |||
| 84712ecc10 | |||
| 0c849d0df4 | |||
| 3120edfe04 | |||
| eaa7e3870b | |||
| c775e8ae05 | |||
| d524118759 | |||
| 643a1a0080 | |||
| b6159aabae | |||
| 52f1205822 | |||
| f86f5189f0 | |||
| f77f57dd17 | |||
| afd4c16763 | |||
| 9cad1c19c0 | |||
| ab6c352538 | |||
| 83bd86dd6d | |||
| ea117b1654 | |||
| 8cbdea57d8 | |||
| 8028b93f53 | |||
| d9cee38906 | |||
| b0ba29ab3c | |||
| e248b2aacf | |||
| b9b2ebe202 | |||
| 2ecdfcdbd2 | |||
| 2077f1de21 | |||
| 3f33497c8e | |||
| 5a35284f98 | |||
| 019bff5738 | |||
| bf087c49a1 | |||
| 6617ecb114 | |||
| 2c5b3d2c07 | |||
| 141d9fe4a6 | |||
| 19532428d0 | |||
| 845315f52c | |||
| f22e43e189 | |||
| 1efdb846f3 | |||
| a5d34306e5 | |||
| 001c1fdc59 | |||
| a01984cbef | |||
| 11d6916841 | |||
| be03a21214 | |||
| 611c5de9f3 | |||
| 9d97391c54 | |||
| e109797420 | |||
| bf5ae85b6b | |||
| f3f968e995 | |||
| b54c6ff5c5 | |||
| 8f58ee37ca | |||
| 12b2ee43d4 | |||
| 54c846fed6 | |||
| 9a975fae43 | |||
| a1b286acea | |||
| bd0ddc26cc | |||
| 2ad69dcd93 | |||
| 154f46a631 | |||
| ab0a45a394 | |||
| 6fa8de468b | |||
| ae5df83c5d | |||
| 3f54f001b3 | |||
| 86a6aa5014 | |||
| 29fa26a9fc | |||
| 11fc5248c2 | |||
| 45596e29cd | |||
| 13637ef8f3 | |||
| b6962fa0f7 | |||
| 78f3ba06ed | |||
| 98b562e2e6 | |||
| a9fc6a2cba | |||
| 139bd32224 | |||
| 16ddff1d1a | |||
| d47cf5fd60 | |||
| 7675048563 | |||
| e8b7591e7c | |||
| 0daf926740 | |||
| 6cc9d610f1 | |||
| 512345fd41 | |||
| 6dcfef639c | |||
| 3c08be0168 | |||
| 38901c7716 | |||
| 46f8c9a702 | |||
| eee5b87a38 | |||
| e1bc2b7dfa | |||
| 94d654d7d0 | |||
| 248116cc8a | |||
| 13d7381c62 | |||
| 4ae90fc2da | |||
| 1d13fbaff1 | |||
| 6c5995b6ac | |||
| d952b4485d | |||
| 789438690d | |||
| 3ec02c68e2 | |||
| 470b876865 | |||
| 877bfe2df2 | |||
| 11dfcb4c8f | |||
| a396237832 | |||
| 5236ccb61a | |||
| d2de2039d5 | |||
| 7ca757bb85 | |||
| 9e14fe449a | |||
| 7edb5c486a | |||
| f2cf630aa2 | |||
| a2b4d945a2 | |||
| 9b8e16f990 | |||
| e2ff07b388 | |||
| e9a9578735 | |||
| 9e483a317d | |||
| dceb748fbe | |||
| a06bc276c1 | |||
| f3dcf10ace | |||
| c0be926d99 | |||
| a5ed4ac6e9 | |||
| 4b87d754fb | |||
| ec56b30cdc | |||
| ea746b7741 | |||
| fb77bb0b37 | |||
| 46942efe07 | |||
| 9545403e00 | |||
| b089a1f580 | |||
| 332158baaa | |||
| 80f860493a | |||
| 67918900bf | |||
| 0f4e71d478 | |||
| 355a4df65f | |||
| 776c82ccae | |||
| 7f0035a823 | |||
| bcd6bdcd9b | |||
| 7b973f88e8 | |||
| 0c48159244 | |||
| 08e7b0946a | |||
| 1fada45e4c | |||
| f07978cf08 | |||
| e9b24f7313 | |||
| b27d439834 | |||
| ede4da931c | |||
| 843bbbbe58 | |||
| e0fcc8ae4b | |||
| 6a3459e514 | |||
| 6c580646f3 | |||
| e00671d697 | |||
| ca0ac18a62 | |||
| fd4ada4f4d | |||
| feea08adee | |||
| 93e003b31e | |||
| ab54721c04 | |||
| b408b7ff35 | |||
| ce323ca60a | |||
| e1801b7a99 | |||
| 6a28961dde | |||
| 0d6abb9850 | |||
| 8c3e369599 | |||
| 3c1b01a857 | |||
| f7d3f611cd | |||
| 84b45aad46 | |||
| d1fa514499 | |||
| 371eea50bf | |||
| d32f133d98 | |||
| 5d7832bec1 | |||
| 3a55daed2f | |||
| 195c5ab21a | |||
| 74045b7de1 | |||
| 1c1a4d8af6 | |||
| 49280f616a | |||
| db3df9a3ea | |||
| ad7afe8646 | |||
| c4a3240c22 | |||
| c37830697a | |||
| 81fa792198 | |||
| 155baa346b | |||
| 26e9589842 | |||
| b493355cbc | |||
| 4062872299 | |||
| 4d9af6651a | |||
| a8b50642f2 | |||
| f8ed17dd58 | |||
| aecba53de5 | |||
| ee62e9c2e7 | |||
| d4313fd6e5 | |||
| 398d9b0343 | |||
| 867d1dfcbe | |||
| d853598c5f | |||
| f6a74731ba | |||
| de2d200c89 | |||
| 8057b2454c | |||
| a9b257c9ca | |||
| d9e93f9110 | |||
| fe2856195e | |||
| aca618fb1e | |||
| 30456d68a5 | |||
| 96ddf076eb | |||
| 97c8c2460e | |||
| 8b15dbdd5b | |||
| ea3726f88b | |||
| 14478919e6 | |||
| e0d7238a10 | |||
| a6301d2b6c | |||
| 455fbf36e0 | |||
| bdd26c7d17 | |||
| 82ede09908 | |||
| be574d371f | |||
| a0e85f5203 | |||
| f8d0438c06 | |||
| 04eb179899 | |||
| d4ffba86a6 | |||
| 200949d49f | |||
| 97dbf0ee7b | |||
| a1b4986060 | |||
| bbc9d35d53 | |||
| 5d918b0fad | |||
| ca10b2103a | |||
| a76c4b9b56 | |||
| 6ac297bac5 | |||
| 458a758ea7 | |||
| 6d7f9b10bd | |||
| 1994ca1ac7 | |||
| 47fe89a595 | |||
| c6c96fd51f | |||
| 060737b0d5 | |||
| e3555236d4 | |||
| d93793eb81 | |||
| 01c0bd0e73 | |||
| 62a56c455a | |||
| 3e4adc29f7 | |||
| bd2bc5b264 | |||
| 17cc6becd2 | |||
| 30d14f5359 | |||
| e7053c2790 | |||
| a0d71bb8b0 | |||
| 4d44a1ceb9 | |||
| 306aeb3225 | |||
| feb7366124 | |||
| 423c6f2f85 | |||
| 22633dc16e | |||
| 9bbd1af259 | |||
| 2cb698c6bd | |||
| d292d5d419 | |||
| 2caac75dbd | |||
| d75d1a717c | |||
| f06c0530ce | |||
| 59f257346d | |||
| 7c15c26fa9 | |||
| 56c54f1ab1 | |||
| da0c07ff33 | |||
| 9b882499e8 | |||
| 4d2d04c232 | |||
| a723e3a4dd | |||
| 4dbd794b41 | |||
| 1a406c4d7d | |||
| baf543ba00 | |||
| e06400bb71 | |||
| 1ee6560f30 | |||
| 5e5948ecd4 | |||
| 7b768d6149 | |||
| e9029eb1f9 | |||
| a7783fdb0d | |||
| 7bc76f2f34 | |||
| 7472e78755 | |||
| 0e5f8e75f9 | |||
| 0fdb7f0a93 | |||
| cc0705183a | |||
| 2aee2c9e27 | |||
| ccc45a41e6 | |||
| b27b4a38eb | |||
| 90112de6e4 | |||
| cad08380ea | |||
| 2aecf0c96a | |||
| e614703305 | |||
| a2f1a1feb3 | |||
| b0965b3ec7 | |||
| 9f9f745f47 | |||
| 52ab35d8c6 | |||
| 5f78722c8f | |||
| 3da97fb7cb | |||
| 9c191c6c11 | |||
| 095d9fd7fa | |||
| 2df8769fcf | |||
| 043d6692f5 | |||
| 10a377e083 | |||
| 042d6099c4 | |||
| e001a21e4b | |||
| 5aa6e18ea7 | |||
| 841c9bc261 | |||
| 218450880e | |||
| fcee182ca3 | |||
| 8590148803 | |||
| d93c9b3c59 | |||
| 4a238256e8 | |||
| 5718775bf7 | |||
| 89754a62fe | |||
| ecb93cb115 | |||
| e79c90f330 | |||
| 691013d7e0 | |||
| 5b3e800567 | |||
| a3c2fcf1b6 | |||
| 5d6a794d52 | |||
| a54a404dac | |||
| a3ea2a32f1 | |||
| 0148a46244 | |||
| c680428b3c | |||
| 247dcbfe11 | |||
| 95e2b726c1 | |||
| fd3fb23955 | |||
| c56c43c464 | |||
| 445325453b | |||
| d072682e82 | |||
| a3245278f0 | |||
| 80d00577e5 | |||
| 2dd46b31a2 | |||
| 84bc28b371 | |||
| 8b0fbd8e77 | |||
| 375978b526 | |||
| 1487823641 | |||
| a68a4ce36b | |||
| 81c393153b | |||
| e4076d7a75 | |||
| d5201a29da | |||
| f06b4e5b1d | |||
| 11cc074a09 | |||
| 737b9fb73e | |||
| d0f0dc7339 | |||
| 09368dd267 | |||
| f964178682 | |||
| ba7ef8e7f0 | |||
| 4e2a5e6f15 | |||
| f54ffa796f | |||
| 3e00e924f8 | |||
| 592c50ba75 | |||
| 5c79ac8893 | |||
| 01dddade5a | |||
| b83f263919 | |||
| 82e8a893fd | |||
| 27236a5692 | |||
| 6e4b9d8196 | |||
| 36df6b9e1e | |||
| ab1d3f41fa | |||
| dbeb523882 | |||
| aae2a36d1e | |||
| 2d4323a72c | |||
| da008874dc | |||
| ae36ce07d1 | |||
| b9ef941b80 | |||
| 465fc427d6 | |||
| 850ff87849 | |||
| bc45423eca | |||
| 92cb5f3583 | |||
| 9a5bd8a846 | |||
| 4393143ee8 | |||
| ddb29fd85b | |||
| c04951c45e | |||
| ec50163b66 | |||
| 9c7241e9ac | |||
| 3b5c0c2e63 | |||
| c69c8b57c4 | |||
| c9628970d9 | |||
| ee0c50bea2 | |||
| 3c527b7064 | |||
| 0bd250a34b | |||
| ff5ad8b062 | |||
| 3b38889f32 | |||
| a6f202be04 | |||
| 9161e5f7e8 | |||
| 12e32cc8ff | |||
| 394b784106 | |||
| a3a928367b | |||
| bacdf2c87c | |||
| 53e50912e6 | |||
| 5d5c712f1c | |||
| 050ea48e3e | |||
| 09e07868bb | |||
| 613ac16601 | |||
| 84cf5809a0 | |||
| 20b42042da | |||
| 10fee49e9a | |||
| b18f42b372 | |||
| 515d93f5ef | |||
| 10d1bb861a | |||
| 0abf1e76d4 | |||
| 168636e493 | |||
| 4dffce0b71 | |||
| a547d0dc05 | |||
| 19d0af9e71 | |||
| 14e1e2fc71 | |||
| 7e02996b28 | |||
| 68c67f351e | |||
| 818185bf5b | |||
| 26847d52f7 | |||
| f1625ed345 | |||
| 244d84e168 | |||
| 6c01a5d9bb | |||
| a1b6b20bdd | |||
| 530bef34f0 | |||
| f85e787e01 | |||
| 44ce9024eb | |||
| a79c435bdc | |||
| 5f961bada4 | |||
| a63e0ed287 | |||
| 99f096c971 | |||
| 2ccbe61b27 | |||
| 282edca008 | |||
| 09840df51a | |||
| 10a459726a | |||
| b9501d69a5 | |||
| 34cd750121 | |||
| 8ccbb03d69 | |||
| 16d209e0c8 | |||
| d61e3407b4 | |||
| ca27288d43 | |||
| d787c430ce | |||
| 7fb4fbdaf3 | |||
| 8dc0236e89 | |||
| 5f0ff047d4 | |||
| bdbbc78497 | |||
| 0980a394b7 | |||
| 5cd23ff19f | |||
| 79e4d6e317 | |||
| 2ae80312ee | |||
| cec4d51649 | |||
| f39fd36b85 | |||
| ad94b75607 | |||
| 670f34fde6 | |||
| bfb22c04b7 | |||
| 550d6b9868 | |||
| abb5a2c985 | |||
| d377a23ebc | |||
| 9ca6ac7080 | |||
| 3098917d55 | |||
| b7bc5d38bd | |||
| 891cb135f4 | |||
| 6427f16743 | |||
| 1efcfc69f4 | |||
| 25e59adb2a | |||
| eb7530b5fd | |||
| 419e1d65e4 | |||
| bec228b854 | |||
| 9e853c07eb | |||
| 4d2ba3b082 | |||
| 5f0889c281 | |||
| 36552f651d | |||
| 64ea5e827b | |||
| da6ee44a2a | |||
| cc34847de9 | |||
| 567f430b7b | |||
| ccd481e64a | |||
| f420107704 | |||
| cd4b6c448c | |||
| 3e04da7062 | |||
| f47015223c | |||
| 2935fa6a36 | |||
| 436c54b829 | |||
| 7e970a175d | |||
| e18b522ff3 | |||
| 77cb64369b | |||
| bd4423c9c6 | |||
| af390db21e | |||
| 1f3eeb4f43 | |||
| 778317aa8a | |||
| c12dfcef54 | |||
| 5880101d9a | |||
| 13cc4fb045 | |||
| 66450913b5 | |||
| 3d55cfaaca | |||
| c435d4d35a | |||
| 719b7485bd | |||
| 7d60ef5fd6 | |||
| 6820a3def1 | |||
| 364fe2f29f | |||
| b146d78525 | |||
| 012968003f | |||
| 0d9bc325fa | |||
| 3a26f63c2a | |||
| de74b05703 | |||
| df24a6ab32 | |||
| 08e8ce4d75 | |||
| d627b6b0b0 | |||
| ed395d74bc | |||
| 1c5d56c28f | |||
| 370f8d7cad | |||
| a33e662c47 | |||
| d881dd6228 | |||
| 43c0cd034c | |||
| 8fa890e0d0 | |||
| 98f6871d8b | |||
| fa1104fd75 | |||
| 2e17e444aa | |||
| 7835785aac | |||
| 3a031064fb | |||
| 83e425dccc | |||
| 1b09a80caa | |||
| 32d7b2fe34 | |||
| e0f6ddfcf7 | |||
| 2086444a9e | |||
| 117b58fcbe | |||
| 3d441748c4 | |||
| 7f3eae23a1 | |||
| 8392642f5f | |||
| e15639583d | |||
| 2ae62957d4 | |||
| 9678b7d966 | |||
| 896f55f6d3 | |||
| 74faa29012 | |||
| 5fc8a50ae1 | |||
| ee68315e70 | |||
| d16d8729c0 | |||
| b28fb72670 | |||
| 1b6e157525 | |||
| 383147b26a | |||
| 6ede67512d | |||
| 57b19457e4 | |||
| 9a41594ec3 | |||
| b5ef4e0e6d | |||
| d7a2732dc6 | |||
| f594abaa71 | |||
| 9c35be2fdc | |||
| 79bd284179 | |||
| 8e6ceb2e66 | |||
| 07c9699895 | |||
| af0d78e720 | |||
| 46dabee6ee | |||
| a723cee47f | |||
| bbf71d8e88 | |||
| 0867924a01 | |||
| 26662b9ed9 | |||
| 29f7b771a1 | |||
| de5c1ca1cf | |||
| 94040cf3f9 | |||
| 2e187ce012 | |||
| 71e4b687b9 | |||
| dd3522a34c | |||
| 0bed8c89f6 | |||
| 98ac637ada | |||
| 82e77d36a6 | |||
| b0f9ba5483 | |||
| 39b50b2aa9 | |||
| 4170bf3a7a | |||
| 632bb64b96 | |||
| a33760a688 | |||
| 0abddd8665 | |||
| 1dc1cb92b9 | |||
| 0adcc2af4f | |||
| 24397aa25e | |||
| fbaa3ad15f | |||
| aa2d357de5 | |||
| 397a2b8803 | |||
| b63d1fa8e1 | |||
| 6590c06f5b | |||
| b608859ef9 | |||
| c9ae24cf62 | |||
| e1f5fea303 | |||
| 8ef05c850d | |||
| 05a558165a | |||
| 68c36ca353 | |||
| 6c79aaae49 | |||
| 35ba5fc766 | |||
| 6d32ab6095 | |||
| ed96c83953 | |||
| 7108d0fe31 | |||
| bc4bce6db5 | |||
| c9fd832af1 | |||
| fa54de2960 | |||
| 58a60d3bd5 | |||
| 3dd8642a60 | |||
| 734542b3c4 | |||
| 89e62541b5 | |||
| cf620ee355 | |||
| 7269e2c64f | |||
| 357d4d72d7 | |||
| cd3fbda09c | |||
| 9b8d8ed544 | |||
| 424f6b2b1b | |||
| 709a12e74c | |||
| 46ffcdbf75 | |||
| 847751e3e0 | |||
| 54724e209a | |||
| d5ab13a00e | |||
| 529d445d06 | |||
| 72f6a3213e | |||
| ec32711e7e | |||
| 764fe92f1b | |||
| 68a9170251 | |||
| 1581b5bbfd | |||
| 7e0bd28bc2 | |||
| 06b25501a0 | |||
| 6be1d3bacb | |||
| c211428897 | |||
| 786a1d0c2a | |||
| dcecd166d0 | |||
| ba59fbac48 | |||
| c704129975 | |||
| 259d70c63e | |||
| e438e8e9ac | |||
| e8091cb5f3 | |||
| 949ec91cc8 | |||
| bc62eaef90 | |||
| 6e1b5cacd6 | |||
| 7f57c18444 | |||
| 3579992f62 | |||
| 3b7ec409e9 | |||
| d885bfa93f | |||
| c0f4f8979a | |||
| 728aa9dc35 | |||
| 38fc329b9f | |||
| f252b01ae2 | |||
| 2c29eecc22 | |||
| 3125e1b386 | |||
| cac8659fbd | |||
| 1230a7bb3c | |||
| 1d57d4d9a4 | |||
| 2e12d1765a | |||
| fb03e34765 | |||
| b1addd05dc | |||
| 5c636cca0b | |||
| c4d8699f91 | |||
| 8bbcb119ce | |||
| 15b9ce0ee3 | |||
| fd8077d9f5 | |||
| 360e6e36cd | |||
| 51b0f603c5 | |||
| 01de0250ac | |||
| e43160f972 | |||
| 74a65946b4 | |||
| a46f7341e7 | |||
| e6814ca4d2 | |||
| 3fdbcb436b | |||
| 6f12fb200c | |||
| 04456e2d8c | |||
| 0dcf82a120 | |||
| 5435f3f9c1 | |||
| 5d2f3d51bf | |||
| 5ee64c827a | |||
| a0e7cf59b6 | |||
| 722f45050a | |||
| 7f3732d43c | |||
| 9ac5889262 | |||
| 4fd1f31ab7 | |||
| 42d8dae900 | |||
| c2799658ba | |||
| bf2965dba0 | |||
| 50433206a3 | |||
| b9b31afeca | |||
| 29f7478d86 | |||
| 47de719fc8 | |||
| 5f716669fe | |||
| 302a45d6e0 | |||
| 9becad268d | |||
| 017e8bf0e5 | |||
| 5617e19e7c | |||
| 2a14f6c441 | |||
| b7e235b727 | |||
| bc6ff607ce | |||
| bbd6b47015 | |||
| fb4025812a | |||
| 13ab5c1345 | |||
| 4cebc58576 | |||
| a80ddc17a7 | |||
| e50ff5e364 | |||
| ec00eecab3 | |||
| 89cde563f6 | |||
| a809d3fecb | |||
| 2ddb26761a | |||
| f8229ba53b | |||
| f0a5796f2e | |||
| 5f6344f400 | |||
| 8550b88dfe | |||
| 85ff6a02bf | |||
| b5fa60235d | |||
| 6a43e184e8 | |||
| 8b67b3b884 | |||
| df20ac9190 | |||
| b6ef7cbb96 | |||
| 1236b64081 | |||
| c15a8dba6c | |||
| 93a7137d10 | |||
| 61c1622cbf | |||
| 6298479e17 | |||
| 3bd55bbeba | |||
| cf1311fe90 | |||
| d56653d946 | |||
| 0b919bfd13 | |||
| 1d7921a760 | |||
| 20af9ed911 | |||
| 849ddd39e0 | |||
| a83dcb7c75 | |||
| 8ef96aefac | |||
| 7bad9f3e3e | |||
| f2aade3b36 | |||
| c668b9274a | |||
| beec65bdb6 | |||
| b71e68581d | |||
| 7a207650d3 | |||
| 53d39608b8 | |||
| 9912698a71 | |||
| be45a75e4d | |||
| 73c43b350a | |||
| 75ad644769 | |||
| 04299a7436 | |||
| da726872ef | |||
| 68f3441fbc | |||
| 671b9f235b | |||
| 377c2f678e | |||
| f8f0c50ed8 | |||
| 23592e19ad | |||
| 409e4beaaa | |||
| 62b369ff8a | |||
| d557c8d9eb | |||
| 4457b5879b | |||
| f586791c71 | |||
| 210d522ec3 | |||
| e54c8bbf99 | |||
| bd96073429 | |||
| 3ebf250ad9 | |||
| 920181752d | |||
| 660b92cd3b | |||
| c7c16ee167 | |||
| 1e6ea77a8f | |||
| 58e4bd1077 | |||
| 2c53dc9514 | |||
| ecf1852367 | |||
| 57e8faa8ab | |||
| 7948d68ac7 | |||
| 48f089e136 | |||
| 4432e65e8a | |||
| 10aded5de4 | |||
| 5481a65ab1 | |||
| 75b867550b | |||
| 23f1b0f584 | |||
| 790b8bed42 | |||
| 1e35b621eb | |||
| b14828e8e1 | |||
| 4274b8f459 | |||
| 41e8bcd02f | |||
| 2fe86f9b8a | |||
| 589f19f370 | |||
| 8e20db664f | |||
| fdcd457ce1 | |||
| 95516a2383 | |||
| ba92b1e667 | |||
| f3a159823a | |||
| 8388491e58 | |||
| e87d206dda | |||
| db4c8d92da | |||
| daab4a95c2 | |||
| 22b8b9b9bd | |||
| c87f3a8cb4 | |||
| 72118a0b66 | |||
| 68573ceb18 | |||
| 510b88cd68 | |||
| 490720e6a7 | |||
| 990f75dddc | |||
| a3c6b82283 | |||
| f5e0ff51f2 | |||
| f114a629f3 | |||
| 5fca372ddf | |||
| d9d1f13bf9 | |||
| 63b212bea5 | |||
| 5a1e09936f | |||
| e21a504c35 | |||
| 3ba6c387e9 | |||
| 2c7238b2c9 | |||
| 92b9fc02fa | |||
| 576281990b | |||
| 6b7570df4e | |||
| b141db4776 | |||
| 4cffcfff03 | |||
| 59ea292263 | |||
| e0ca52b1da | |||
| 0c9ea1e0f0 | |||
| c02cf0f5dc | |||
| d0e2df5166 | |||
| b9cda71413 | |||
| e008e44566 | |||
| c100539736 | |||
| 32aa3febf9 | |||
| 1249b3b3e8 | |||
| 18ba66afcc | |||
| 1000d88508 | |||
| e13cb1debd | |||
| 2c3c8f8c4a | |||
| b81196fa87 | |||
| c7291af970 | |||
| 92c3237552 | |||
| 848e446b93 | |||
| 2f96f565eb | |||
| 8fa58eb108 | |||
| 31947127d9 | |||
| 2c7cfa1a93 | |||
| b856c4f995 | |||
| 497be710a7 | |||
| d7287b5c3c | |||
| 854010b823 | |||
| 39f7a5be70 | |||
| dbc53b8d09 | |||
| c4fe362a08 | |||
| f55ec5de9b | |||
| b2279c9acc | |||
| b420d054ae | |||
| 566f0f7783 | |||
| ae24c1d968 | |||
| 8ca344e3bf | |||
| 0458d2cb90 | |||
| 7c2322e6e0 | |||
| 08abe4bff2 | |||
| eb69c365fc | |||
| f6fef21bf7 | |||
| 4a1f8457cf | |||
| 5eb5b952d5 | |||
| 8a375c6363 | |||
| ac23b610bc | |||
| 5f8b141f62 | |||
| 517db50712 | |||
| 6310a431dd | |||
| 9996e9a6d7 | |||
| ddc211a8ea | |||
| 32f4f88b88 | |||
| 45b3062ac6 | |||
| 03296b3195 | |||
| 97df39a16f | |||
| 59cd6f6e93 | |||
| d4312507e2 | |||
| 76950bdada | |||
| 01b7bc96fa | |||
| efde15b848 | |||
| 941e0ba6c8 | |||
| 3b818855dc | |||
| f73c8b00d4 | |||
| 08f116486a | |||
| f6f5ae8578 | |||
| d82dde4b7f | |||
| 91d4d95cb4 | |||
| b9973d69c3 | |||
| 8c8e363abc | |||
| aa240e8ee3 | |||
| cdaf9e1876 | |||
| 1c8352ec56 | |||
| 43ef7f088d | |||
| 28b4f66f86 | |||
| 4fb94ea162 | |||
| d24340f221 | |||
| 482cd123c0 | |||
| ab3abe7e5e | |||
| 31fbffb435 | |||
| 9a7f8bd861 | |||
| 29c20cfcc4 | |||
| b5c25bcaaa | |||
| 8abe0a174a | |||
| 692abcd6de | |||
| 03bdcc786e | |||
| 6df2985e2a | |||
| 05de8b54ec | |||
| c0dad4f5a0 | |||
| 7ad425e399 | |||
| 836a3784cb | |||
| 06d4aec850 | |||
| 614674563a | |||
| 349633c8da | |||
| 7d4f617757 | |||
| e82f17ab06 | |||
| cb14592705 | |||
| 77300d6858 | |||
| 38682e48d4 | |||
| 1e5d28e2a2 | |||
| ad86b4b1eb | |||
| 99927df991 | |||
| 6661f21e2f | |||
| 4ef963fe54 | |||
| c87ddd5116 | |||
| 4f4df7d9fe | |||
| 0043b3690a | |||
| be6c34386d | |||
| a8e9a71489 | |||
| 90f42fe6cd | |||
| 6dd414fe7e | |||
| 4cb5e66ccb | |||
| 1fd4d772e4 | |||
| 3abdbdc7c9 | |||
| 6d6fba873f | |||
| 6aa8602b96 | |||
| 240272f7ce | |||
| 3d17a33c43 | |||
| 6956cfa32d | |||
| 3a54e662c2 | |||
| 823cfca3c3 | |||
| 9da2484bab | |||
| 0b50d62ef3 | |||
| 343e8e90ba | |||
| 02dcb013ef | |||
| e77d3f4fcc | |||
| 7aff747b1c | |||
| e97f3032cc | |||
| ebabe29d8e | |||
| b690c9bc95 | |||
| fd3034bacc | |||
| 3bcef3d9c3 | |||
| da54699815 | |||
| 6b64dd52b9 | |||
| fb07dc2294 | |||
| 779c3ba75b | |||
| 4564e501d3 | |||
| d271d2db57 | |||
| 46ed0ab49e | |||
| 35dfea03da | |||
| ff5036a55b | |||
| 799892c220 | |||
| 8b160cbbfd | |||
| 48983879ab | |||
| 2cecdd7f01 | |||
| 4ebaa674c3 | |||
| fb637f61f3 | |||
| 805e07e65f | |||
| 049a488e08 | |||
| afc90817cf | |||
| 38f3e39258 | |||
| c674d679bd | |||
| 7c2ab4e5bd | |||
| b86dff8601 | |||
| a725fc7a0b | |||
| fbe3545153 | |||
| 50b528260c | |||
| d2ece2b7f9 | |||
| f71e47aac7 | |||
| 8d9c4b0476 | |||
| ea1a62c3ef | |||
| 2e5e459094 | |||
| f51eccdef7 | |||
| a9a9af9ef7 | |||
| 200122deee | |||
| 4170be7f34 | |||
| 0be5a292c4 | |||
| 4555586254 | |||
| 173531b767 | |||
| 412082d3ef | |||
| 3b51b84308 | |||
| d6d1ad98e4 | |||
| e8560e6905 | |||
| ccaabd6f06 | |||
| 9ba79cfb32 | |||
| 62e0e34e12 | |||
| 2d50ae4b00 | |||
| 11b567391c | |||
| e50e488c8a | |||
| 2a9d32309e | |||
| de0370011c | |||
| 4a844e582e | |||
| d36aad4adc | |||
| 11240b6bbb | |||
| b52d3231e4 | |||
| c9ba4ba50a | |||
| 4db07b5254 | |||
| 83688f9fd8 | |||
| 7a384846f8 | |||
| 923f7f3aa8 | |||
| 8e0cfcda88 | |||
| cd90af35a1 | |||
| d2ac8536b3 | |||
| 5100a28ff1 | |||
| 0830e9293d | |||
| 4a981cd2e2 | |||
| b1d956f7bf | |||
| 75b2c7236a | |||
| c8278e7b24 | |||
| dbf6520860 | |||
| e593e48d40 | |||
| 39bccea953 | |||
| 98f62eba9d | |||
| 4e65728979 | |||
| b58ca1506e | |||
| e0334b3ac8 | |||
| 0fa230527c | |||
| 13c5085cb1 | |||
| 300a3919ab | |||
| e65d946633 | |||
| 412bd1c1f4 | |||
| 1d15fd3178 | |||
| cb94737519 | |||
| 01683e9383 | |||
| 1960969325 | |||
| b49721f514 | |||
| 6876e82d64 | |||
| 15a7beae57 | |||
| 297a635613 | |||
| e0778c52e8 | |||
| e09b9964be | |||
| 1d27926220 | |||
| 7427d549cc | |||
| 37aeb3f713 | |||
| 7bf06da9f8 | |||
| b6157d58c8 | |||
| 4767fe5515 | |||
| 750acdbcd7 | |||
| 29543fbc85 | |||
| 05913d0ae0 | |||
| a31617fcb0 | |||
| ec71b622fc | |||
| 3dd659639d | |||
| 4aca2b64b9 | |||
| 4c2c27c686 | |||
| 429f45a09a | |||
| 886c668107 | |||
| c0df62cd5b | |||
| a8e6d727fa | |||
| ccf1c78cbb | |||
| 4e25688dd9 | |||
| 3378bf7a1e | |||
| 2bbafb5604 | |||
| 1e82774460 | |||
| dce865c3cb | |||
| 81bf84b50a | |||
| 94b6f5bffd | |||
| 5440a3b62b | |||
| 24737382f9 | |||
| 5fa3215a4d | |||
| 105141be53 | |||
| e19edcb67a | |||
| be0b61a628 | |||
| 8d79244068 | |||
| 8ee66d3abf | |||
| fb94416b1b | |||
| 70a925b416 | |||
| 959f245ce4 | |||
| b3eb650315 | |||
| bdf7da6ef6 | |||
| 36d49b8217 | |||
| 18ac61e8ab | |||
| b524da23d5 | |||
| eeac846f5a | |||
| 0410ba51ca | |||
| ca3bf6fe5c | |||
| 4353a05350 | |||
| d2a3bb7339 | |||
| 589ee2d0c5 | |||
| 2178dcc963 | |||
| f18fdd4a46 | |||
| 4352d9c698 | |||
| 494884595c | |||
| b17db02f9d | |||
| 0f33a6b34b | |||
| 231dfe70d0 | |||
| 79eecd8b3e | |||
| ca09f64c12 | |||
| dea1f01998 | |||
| 8cfae92c24 | |||
| 989a5ba685 | |||
| a9e49d98fd | |||
| f66d4e34d6 | |||
| 989820183c | |||
| 53f0e6c7d3 | |||
| 1608faecea | |||
| 4260082726 | |||
| ca573dec91 | |||
| 3e252e1fd8 | |||
| 7adc153e57 | |||
| ae105d9f83 | |||
| 87c895bd76 | |||
| 034b2b2ddd | |||
| fb5a789f55 | |||
| 2b36a2f8cb | |||
| d2a81ce907 | |||
| 1f0b0d7bd1 | |||
| 735527a0f0 | |||
| 4dc034dd5e | |||
| 4bfe4079cc | |||
| 66eff3a020 | |||
| 401c561238 | |||
| 606fe87ca0 | |||
| f4775cc17c | |||
| a2e941970a | |||
| c2ed909818 | |||
| c38c440e63 | |||
| 29b0785594 | |||
| e15dcd41db | |||
| 6528461873 | |||
| a8f5b5d4e4 | |||
| be489744c9 | |||
| cd0b7ed3d2 | |||
| 3ebc5c6b9d | |||
| 66ada600b7 | |||
| 4871d5df9d | |||
| 7088e6682b | |||
| babe0adffb | |||
| 8f0a76ecef | |||
| 23607c303c | |||
| 884b7062c9 | |||
| 07650d424a | |||
| 218ec9c678 | |||
| 8b7c3308b3 | |||
| ca9528fa4e | |||
| aef625ba31 | |||
| e5c8f2caec | |||
| 5c06305f85 | |||
| 428893d5c5 | |||
| fc7277a542 | |||
| c8c6b15285 | |||
| 0a987bdec9 | |||
| ecc4fee84e | |||
| 4802ecfc29 | |||
| 436f415d9f | |||
| 164480834a | |||
| 68642e056c | |||
| 9033c6e1d4 | |||
| 89fc6feb5f | |||
| 80dc9568ce | |||
| 5774a7893f | |||
| abd9ea9ec5 | |||
| 8799882f09 | |||
| f85a4878de | |||
| ae87213105 | |||
| 33bd86a2c7 | |||
| 2092ae22dc | |||
| aa9317069a | |||
| a31ea92649 | |||
| b8f18bdec2 | |||
| 704977d5f6 | |||
| 0757c20d59 | |||
| fa08847d6d | |||
| f91f08628a | |||
| 9ebf6b06dd | |||
| 357d5e46a3 | |||
| c0f5526801 | |||
| 861204e442 | |||
| eb90b614ea | |||
| d087ed2349 | |||
| 6ee7e75465 | |||
| c2b80d7aba | |||
| a95e8633cd | |||
| e3adbbe000 | |||
| eef360673b | |||
| 36e298c758 | |||
| 275157f27b | |||
| e776deaa3f | |||
| 4fc8e9b45e | |||
| fe41eec7c5 | |||
| d1d1d22734 | |||
| da8b76957a | |||
| 305f9fd1cf | |||
| cd2a94ddb8 | |||
| a2df4db504 | |||
| b7740a4758 | |||
| 62c24de5c4 | |||
| 5ed3e67b76 | |||
| c7f2314a15 | |||
| 420c7ebd67 | |||
| b93b1a6eec | |||
| 7d52be6e99 | |||
| 9b1f0e394a | |||
| 1b0cb5d455 | |||
| 9b79d59d93 | |||
| 3e12316ea1 | |||
| 1b38c0111f | |||
| 5542393eb5 | |||
| ad48bc0ee8 | |||
| ba0e5d0b59 | |||
| 1c5ff88e3c | |||
| bf7d4a550e | |||
| 324bc763fc | |||
| f9fb2ca3a1 | |||
| b5eac7c91b | |||
| 3c858ca0fd | |||
| da9d634b83 | |||
| 128704400f | |||
| a3594322bd | |||
| fe4b3d5f1d | |||
| da08da2b54 | |||
| 5deb5f79bd | |||
| 9f0d694f0a | |||
| 4153fb7d1e | |||
| 6994ec0f03 | |||
| e1af60cfa9 | |||
| 7bcec61e6d | |||
| dde287f05d | |||
| 27fc37e55c | |||
| ad901760f6 | |||
| 973029865e | |||
| 52e4fedd16 | |||
| b81ba49370 | |||
| 39a0f93f69 | |||
| 53cb83eacc | |||
| b307d278b0 | |||
| 14348eba38 | |||
| cead5b74ae | |||
| 2e2a945f7c | |||
| 0e3ae2b450 | |||
| 19e2df65ca | |||
| 565d715a66 | |||
| abe6f55aa6 | |||
| c278d0c5d4 | |||
| a7e2c74158 | |||
| d84900d601 | |||
| fdda28d67f |
@@ -4,3 +4,8 @@ node_modules/
|
||||
# vim swap files
|
||||
*.swp
|
||||
|
||||
# these are not done yet
|
||||
src/translation/ja.json
|
||||
src/translation/pl.json
|
||||
src/translation/si.json
|
||||
src/translation/gl.json
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"unused": true,
|
||||
"esversion": 6,
|
||||
"globalstrict": false,
|
||||
"predef": [
|
||||
"$",
|
||||
"angular",
|
||||
"async",
|
||||
"describe",
|
||||
"it",
|
||||
"before",
|
||||
"after",
|
||||
"require",
|
||||
"monaco",
|
||||
"Mimer",
|
||||
"ISTATES"
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
The Cloudron Subscription license
|
||||
Copyright (c) 2019 Cloudron UG
|
||||
Copyright (c) 2022 Cloudron UG
|
||||
|
||||
With regard to the Cloudron Software:
|
||||
|
||||
|
||||
@@ -1,64 +1,20 @@
|
||||
# Cloudron Dashboard
|
||||
|
||||
[Cloudron](https://cloudron.io) is the best way to run apps on your server.
|
||||
This is the front end code of Cloudron. The backend code is [here](https://git.cloudron.io/cloudron/box).
|
||||
|
||||
Web applications like email, contacts, blog, chat are the backbone of the modern
|
||||
internet. Yet, we live in a world where hosting these essential applications is
|
||||
a complex task.
|
||||
## Developing
|
||||
|
||||
We are building the ultimate platform for self-hosting web apps. The Cloudron allows
|
||||
anyone to effortlessly host web applications on their server on their own terms.
|
||||
* `npm install`
|
||||
* `gulp develop --api-origin=https://my.example.com`
|
||||
|
||||
## Features
|
||||
## License
|
||||
|
||||
* Single click install for apps. Check out the [App Store](https://cloudron.io/appstore.html).
|
||||
Please note that the Cloudron code is under a source-available license. This is not the same as an
|
||||
open source license but ensures the code is available for introspection (and hacking!).
|
||||
|
||||
* Per-app encrypted backups and restores.
|
||||
## Contributions
|
||||
|
||||
* App updates delivered via the App Store.
|
||||
|
||||
* Secure - Cloudron manages the firewall. All apps are secured with HTTPS. Certificates are
|
||||
installed and renewed automatically.
|
||||
|
||||
* Centralized User & Group management. Control who can access which app.
|
||||
|
||||
* Single Sign On. Use same credentials across all apps.
|
||||
|
||||
* Automatic updates for the Cloudron platform.
|
||||
|
||||
* Trivially migrate to another server keeping your apps and data (for example, switch your
|
||||
infrastructure provider or move to a bigger server).
|
||||
|
||||
* Comprehensive [REST API](https://cloudron.io/developer/api/).
|
||||
|
||||
* [CLI](https://cloudron.io/documentation/cli/) to configure apps.
|
||||
|
||||
* Alerts, audit logs, graphs, dns management ... and much more
|
||||
|
||||
## Demo
|
||||
|
||||
Try our demo at https://my.demo.cloudron.io (username: cloudron password: cloudron).
|
||||
|
||||
## Installing
|
||||
|
||||
You can install the Cloudron platform on your own server or get a managed server
|
||||
from cloudron.io. In either case, the Cloudron platform will keep your server and
|
||||
apps up-to-date and secure.
|
||||
|
||||
* [Selfhosting](https://cloudron.io/documentation/installation/) - [Pricing](https://cloudron.io/pricing.html)
|
||||
* [Managed Hosting](https://cloudron.io/managed.html)
|
||||
|
||||
## Documentation
|
||||
|
||||
* [Documentation](https://cloudron.io/documentation/)
|
||||
|
||||
## Related repos
|
||||
|
||||
The [base image repo](https://git.cloudron.io/cloudron/docker-base-image) is the parent image of all
|
||||
the containers in the Cloudron.
|
||||
|
||||
## Community
|
||||
|
||||
* [Forum](https://forum.cloudron.io/)
|
||||
* [Support](mailto:support@cloudron.io)
|
||||
Just to give some heads up, we are a bit restrictive in merging changes. We are a small team and
|
||||
would like to keep our maintenance burden low. It might be best to discuss features first in the [forum](https://forum.cloudron.io),
|
||||
to also figure out how many other people will use it to justify maintenance for a feature.
|
||||
|
||||
|
||||
+73
-40
@@ -10,7 +10,7 @@ var argv = require('yargs').argv,
|
||||
execSync = require('child_process').execSync,
|
||||
gulp = require('gulp'),
|
||||
rimraf = require('rimraf'),
|
||||
sass = require('gulp-sass'),
|
||||
sass = require('gulp-sass')(require('node-sass')),
|
||||
serve = require('gulp-serve'),
|
||||
sourcemaps = require('gulp-sourcemaps');
|
||||
|
||||
@@ -35,7 +35,7 @@ if (argv.apiOrigin) {
|
||||
var appstore = {
|
||||
webOrigin: argv.appstoreWebOrigin || '',
|
||||
apiOrigin: argv.appstoreApiOrigin || ''
|
||||
}
|
||||
};
|
||||
|
||||
console.log();
|
||||
console.log('Cloudron API: %s', apiOrigin || 'default');
|
||||
@@ -47,14 +47,8 @@ console.log(' Api: %s', appstore.apiOrigin || 'no');
|
||||
console.log();
|
||||
|
||||
gulp.task('fontawesome', function () {
|
||||
return gulp.src([
|
||||
'node_modules/@fortawesome/fontawesome-free/*css*/all.min.css',
|
||||
'node_modules/@fortawesome/fontawesome-free/*webfonts*/*.eot',
|
||||
'node_modules/@fortawesome/fontawesome-free/*webfonts*/*.svg',
|
||||
'node_modules/@fortawesome/fontawesome-free/*webfonts*/*.ttf',
|
||||
'node_modules/@fortawesome/fontawesome-free/*webfonts*/*.woff',
|
||||
'node_modules/@fortawesome/fontawesome-free/*webfonts*/*.woff2'
|
||||
]).pipe(gulp.dest('dist/3rdparty/fontawesome/'));
|
||||
return gulp.src('node_modules/@fortawesome/fontawesome-free/**/*')
|
||||
.pipe(gulp.dest('dist/3rdparty/fontawesome/'));
|
||||
});
|
||||
|
||||
gulp.task('bootstrap', function () {
|
||||
@@ -62,6 +56,28 @@ gulp.task('bootstrap', function () {
|
||||
.pipe(gulp.dest('dist/3rdparty/js'));
|
||||
});
|
||||
|
||||
gulp.task('monaco', function () {
|
||||
return gulp.src('node_modules/monaco-editor/min/**/*')
|
||||
.pipe(gulp.dest('dist/3rdparty/'));
|
||||
});
|
||||
|
||||
gulp.task('xterm-core', function () {
|
||||
return gulp.src('node_modules/xterm/**/*')
|
||||
.pipe(gulp.dest('dist/3rdparty/xterm'));
|
||||
});
|
||||
|
||||
gulp.task('xterm-addon-attach', function () {
|
||||
return gulp.src('node_modules/xterm-addon-attach/**/*')
|
||||
.pipe(gulp.dest('dist/3rdparty/xterm-addon-attach'));
|
||||
});
|
||||
|
||||
gulp.task('xterm-addon-fit', function () {
|
||||
return gulp.src('node_modules/xterm-addon-fit/**/*')
|
||||
.pipe(gulp.dest('dist/3rdparty/xterm-addon-fit'));
|
||||
});
|
||||
|
||||
gulp.task('xterm', gulp.series(['xterm-core', 'xterm-addon-attach', 'xterm-addon-fit']));
|
||||
|
||||
gulp.task('3rdparty-copy', function () {
|
||||
return gulp.src([
|
||||
'src/3rdparty/**/*.js',
|
||||
@@ -72,11 +88,11 @@ gulp.task('3rdparty-copy', function () {
|
||||
'src/3rdparty/**/*.svg',
|
||||
'src/3rdparty/**/*.gif',
|
||||
'src/3rdparty/**/*.ttf'
|
||||
])
|
||||
])
|
||||
.pipe(gulp.dest('dist/3rdparty/'));
|
||||
});
|
||||
|
||||
gulp.task('3rdparty', gulp.series(['3rdparty-copy', 'bootstrap', 'fontawesome']));
|
||||
gulp.task('3rdparty', gulp.series(['3rdparty-copy', 'monaco', 'xterm', 'bootstrap', 'fontawesome']));
|
||||
|
||||
// --------------
|
||||
// JavaScript
|
||||
@@ -87,8 +103,9 @@ gulp.task('js-index', function () {
|
||||
'src/js/index.js',
|
||||
'src/js/client.js',
|
||||
'src/js/main.js',
|
||||
'src/js/utils.js',
|
||||
'src/views/*.js'
|
||||
])
|
||||
])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('index.js', { newLine: ';' }))
|
||||
@@ -97,17 +114,26 @@ gulp.task('js-index', function () {
|
||||
});
|
||||
|
||||
gulp.task('js-logs', function () {
|
||||
return gulp.src(['src/js/logs.js', 'src/js/client.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, appstore: appstore }, {}, { ext: '.js' }))
|
||||
return gulp.src(['src/js/logs.js', 'src/js/client.js', 'src/js/utils.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('logs.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-filemanager', function () {
|
||||
return gulp.src(['src/js/filemanager.js', 'src/js/client.js', 'src/js/utils.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('filemanager.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-terminal', function () {
|
||||
return gulp.src(['src/js/terminal.js', 'src/js/client.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, appstore: appstore }, {}, { ext: '.js' }))
|
||||
return gulp.src(['src/js/terminal.js', 'src/js/client.js', 'src/js/utils.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('terminal.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
@@ -115,8 +141,8 @@ gulp.task('js-terminal', function () {
|
||||
});
|
||||
|
||||
gulp.task('js-login', function () {
|
||||
return gulp.src(['src/js/login.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, appstore: appstore }, {}, { ext: '.js' }))
|
||||
return gulp.src(['src/js/login.js', 'src/js/utils.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('login.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
@@ -124,8 +150,8 @@ gulp.task('js-login', function () {
|
||||
});
|
||||
|
||||
gulp.task('js-setupaccount', function () {
|
||||
return gulp.src(['src/js/setupaccount.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, appstore: appstore }, {}, { ext: '.js' }))
|
||||
return gulp.src(['src/js/setupaccount.js', 'src/js/utils.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('setupaccount.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
@@ -133,8 +159,8 @@ gulp.task('js-setupaccount', function () {
|
||||
});
|
||||
|
||||
gulp.task('js-setup', function () {
|
||||
return gulp.src(['src/js/setup.js', 'src/js/client.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, appstore: appstore }, {}, { ext: '.js' }))
|
||||
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' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('setup.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
@@ -142,8 +168,8 @@ gulp.task('js-setup', function () {
|
||||
});
|
||||
|
||||
gulp.task('js-setupdns', function () {
|
||||
return gulp.src(['src/js/setupdns.js', 'src/js/client.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, appstore: appstore }, {}, { ext: '.js' }))
|
||||
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())
|
||||
@@ -151,15 +177,15 @@ gulp.task('js-setupdns', function () {
|
||||
});
|
||||
|
||||
gulp.task('js-restore', function () {
|
||||
return gulp.src(['src/js/restore.js', 'src/js/client.js'])
|
||||
.pipe(ejs({ apiOrigin: apiOrigin, appstore: appstore }, {}, { ext: '.js' }))
|
||||
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' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('restore.js', { newLine: ';' }))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js', gulp.series([ 'js-index', 'js-logs', 'js-terminal', 'js-login', 'js-setupaccount', 'js-setup', 'js-setupdns', 'js-restore' ]));
|
||||
gulp.task('js', gulp.series([ 'js-index', 'js-logs', 'js-filemanager', 'js-terminal', 'js-login', 'js-setupaccount', 'js-setup', 'js-setupdns', 'js-restore' ]));
|
||||
|
||||
// --------------
|
||||
// HTML
|
||||
@@ -170,7 +196,7 @@ gulp.task('html-views', function () {
|
||||
});
|
||||
|
||||
gulp.task('html-templates', function () {
|
||||
return gulp.src('src/templates/**/*.html').pipe(gulp.dest('dist/templates'));
|
||||
return gulp.src('src/templates/**/*').pipe(gulp.dest('dist/templates'));
|
||||
});
|
||||
|
||||
gulp.task('html-raw', function () {
|
||||
@@ -198,6 +224,11 @@ gulp.task('images', function () {
|
||||
.pipe(gulp.dest('dist/img'));
|
||||
});
|
||||
|
||||
gulp.task('translation', function () {
|
||||
return gulp.src('src/translation/**')
|
||||
.pipe(gulp.dest('dist/translation'));
|
||||
});
|
||||
|
||||
gulp.task('timezones', function (done) {
|
||||
execSync('./scripts/createTimezones.js ./dist/js/timezones.js');
|
||||
done();
|
||||
@@ -212,28 +243,30 @@ gulp.task('clean', function (done) {
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task('default', gulp.series(['clean', 'html', 'js', 'timezones', '3rdparty', 'images', 'css']));
|
||||
gulp.task('default', gulp.series(['clean', 'html', 'js', 'timezones', '3rdparty', 'translation', 'images', 'css']));
|
||||
|
||||
gulp.task('watch', function (done) {
|
||||
gulp.watch(['src/*.scss'], gulp.series(['css']));
|
||||
gulp.watch(['src/img/*'], gulp.series(['images']));
|
||||
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'], gulp.series(['timezones']));
|
||||
gulp.watch(['src/js/setup.js', 'src/js/client.js'], gulp.series(['js-setup']));
|
||||
gulp.watch(['src/js/setupdns.js', 'src/js/client.js'], gulp.series(['js-setupdns']));
|
||||
gulp.watch(['src/js/restore.js', 'src/js/client.js'], gulp.series(['js-restore']));
|
||||
gulp.watch(['src/js/logs.js', 'src/js/client.js'], gulp.series(['js-logs']));
|
||||
gulp.watch(['src/js/terminal.js', 'src/js/client.js'], gulp.series(['js-terminal']));
|
||||
gulp.watch(['src/js/login.js'], gulp.series(['js-login']));
|
||||
gulp.watch(['src/js/setupaccount.js'], gulp.series(['js-setupaccount']));
|
||||
gulp.watch(['src/js/index.js', 'src/js/client.js', 'src/js/main.js', 'src/views/*.js'], gulp.series(['js-index']));
|
||||
gulp.watch(['scripts/createTimezones.js', 'src/js/utils.js'], gulp.series(['timezones']));
|
||||
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/logs.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-logs']));
|
||||
gulp.watch(['src/js/filemanager.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-filemanager']));
|
||||
gulp.watch(['src/js/terminal.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-terminal']));
|
||||
gulp.watch(['src/js/login.js', 'src/js/utils.js'], gulp.series(['js-login']));
|
||||
gulp.watch(['src/js/setupaccount.js', 'src/js/utils.js'], gulp.series(['js-setupaccount']));
|
||||
gulp.watch(['src/js/index.js', 'src/js/client.js', 'src/js/main.js', 'src/views/*.js', 'src/js/utils.js'], gulp.series(['js-index']));
|
||||
gulp.watch(['src/3rdparty/**/*'], gulp.series(['3rdparty']));
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task('serve', serve({ root: 'dist', port: 4000 }));
|
||||
gulp.task('serve', serve({ root: 'dist', port: 4000, hostname: '0.0.0.0' }));
|
||||
|
||||
gulp.task('develop', gulp.series(['default', 'watch', 'serve']));
|
||||
|
||||
|
||||
Generated
+10844
-1788
File diff suppressed because it is too large
Load Diff
+15
-9
@@ -3,7 +3,8 @@
|
||||
"version": "1.0.0",
|
||||
"description": "[Cloudron](https://cloudron.io) is the best way to run apps on your server.",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"update-translations": "curl https://translate.cloudron.io/api/components/cloudron/dashboard/file/ -o lang.zip && unzip -jo lang.zip -d ./src/translation/ && rm lang.zip"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -12,18 +13,23 @@
|
||||
"author": "",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.5.0",
|
||||
"@fortawesome/fontawesome-free": "^5.15.4",
|
||||
"bootstrap-sass": "^3.4.1",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-autoprefixer": "^5.0.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-autoprefixer": "^8.0.0",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-cssnano": "^2.1.3",
|
||||
"gulp-ejs": "^3.3.0",
|
||||
"gulp-sass": "^4.0.2",
|
||||
"gulp-ejs": "^5.1.0",
|
||||
"gulp-sass": "^5.1.0",
|
||||
"gulp-serve": "^1.4.0",
|
||||
"gulp-sourcemaps": "^2.6.5",
|
||||
"rimraf": "^2.6.2",
|
||||
"yargs": "^11.0.0"
|
||||
"gulp-sourcemaps": "^3.0.0",
|
||||
"monaco-editor": "^0.32.1",
|
||||
"node-sass": "^7.0.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"xterm": "^4.17.0",
|
||||
"xterm-addon-attach": "^0.6.0",
|
||||
"xterm-addon-fit": "^0.5.0",
|
||||
"yargs": "^17.3.1"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"env": {
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ function getAccessToken(callback) {
|
||||
let username = readlineSync.question('Username: ', {});
|
||||
let password = readlineSync.question('Password: ', { noEchoBack: true });
|
||||
|
||||
superagent.post(`https://${cloudronDomain}/api/v1/developer/login`, { username: username, password: password }).end(function (error, result) {
|
||||
superagent.post(`https://${cloudronDomain}/api/v1/cloudron/login`, { username: username, password: password }).end(function (error, result) {
|
||||
if (error || result.statusCode !== 200) {
|
||||
console.log('Login failed');
|
||||
return getAccessToken(callback);
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ function getAccessToken(callback) {
|
||||
let username = readlineSync.question('Username: ', {});
|
||||
let password = readlineSync.question('Password: ', { noEchoBack: true });
|
||||
|
||||
superagent.post(`https://${cloudronDomain}/api/v1/developer/login`, { username: username, password: password }).end(function (error, result) {
|
||||
superagent.post(`https://${cloudronDomain}/api/v1/cloudron/login`, { username: username, password: password }).end(function (error, result) {
|
||||
if (error || result.statusCode !== 200) {
|
||||
console.log('Login failed');
|
||||
return getAccessToken(callback);
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
@keyframes chartjs-render-animation{from{opacity:.99}to{opacity:1}}.chartjs-render-monitor{animation:chartjs-render-animation 1ms}.chartjs-size-monitor,.chartjs-size-monitor-expand,.chartjs-size-monitor-shrink{position:absolute;direction:ltr;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1}.chartjs-size-monitor-expand>div{position:absolute;width:1000000px;height:1000000px;left:0;top:0}.chartjs-size-monitor-shrink>div{position:absolute;width:200%;height:200%;left:0;top:0}
|
||||
Vendored
+7
File diff suppressed because one or more lines are too long
Vendored
-10
File diff suppressed because one or more lines are too long
+11
-4
@@ -1,5 +1,12 @@
|
||||
"use strict";
|
||||
|
||||
// -------------------------------
|
||||
// WARNING
|
||||
// -------------------------------
|
||||
// This file is taken from https://github.com/sebastianha/angular-bootstrap-multiselect
|
||||
// There are local modifications like support for translation
|
||||
// -------------------------------
|
||||
|
||||
angular.module("ui.multiselect", ["multiselect.tpl.html"])
|
||||
//from bootstrap-ui typeahead parser
|
||||
.factory("optionParser", ["$parse", function($parse) {
|
||||
@@ -23,7 +30,7 @@ angular.module("ui.multiselect", ["multiselect.tpl.html"])
|
||||
}
|
||||
};
|
||||
}])
|
||||
.directive("multiselect", ["$parse", "$document", "$compile", "$interpolate", "optionParser", function($parse, $document, $compile, $interpolate, optionParser) {
|
||||
.directive("multiselect", ["$parse", "$document", "$compile", "$interpolate", "$translate", "optionParser", function($parse, $document, $compile, $interpolate, $translate, optionParser) {
|
||||
return {
|
||||
restrict: "E",
|
||||
require : "ngModel",
|
||||
@@ -154,7 +161,7 @@ angular.module("ui.multiselect", ["multiselect.tpl.html"])
|
||||
|
||||
function getHeaderText() {
|
||||
if(isEmpty(modelCtrl.$modelValue)) {
|
||||
scope.header = attrs.msHeader || "Select";
|
||||
scope.header = attrs.msHeader || $translate.instant('main.multiselect.select');
|
||||
return scope.header;
|
||||
}
|
||||
|
||||
@@ -162,7 +169,7 @@ angular.module("ui.multiselect", ["multiselect.tpl.html"])
|
||||
if(attrs.msSelected) {
|
||||
scope.header = $interpolate(attrs.msSelected)(scope);
|
||||
} else {
|
||||
scope.header = modelCtrl.$modelValue.length + " " + "selected";
|
||||
scope.header = $translate.instant('main.multiselect.selected', { n: modelCtrl.$modelValue.length });
|
||||
}
|
||||
} else {
|
||||
var local = {};
|
||||
@@ -342,7 +349,7 @@ angular.module("multiselect.tpl.html", []).run(["$templateCache", function($temp
|
||||
" <div ng-style=\"maxWidth\" style=\"padding-right: 13px; overflow: hidden; text-overflow: ellipsis;\">{{header}}</div><span class=\"caret\" style=\"position:absolute;right:10px;top:14px;\"></span>\n" +
|
||||
" </button>\n" +
|
||||
" <ul class=\"dropdown-menu\" style=\"margin-bottom:30px;padding-left:5px;padding-right:5px;\" ng-style=\"ulStyle\">\n" +
|
||||
" <input ng-show=\"items.length > filterAfterRows\" ng-model=\"filter\" style=\"padding: 0px 3px;margin-right: 15px; margin-bottom: 4px;\" placeholder=\"Type to filter options\">" +
|
||||
" <input ng-show=\"items.length > filterAfterRows\" ng-model=\"filter\" style=\"padding: 0px 3px;margin-right: 15px; margin-bottom: 4px;\" placeholder=\"{{ 'main.multiselect.filterPlaceholder' | tr }}\">" +
|
||||
" <li data-stopPropagation=\"true\" ng-repeat=\"i in items | filter:filter\" ng-class=\"{'dropdown-header': i.header, 'divider': i.divider}\">\n" +
|
||||
" <a ng-if=\"!i.header && !i.divider\" ng-click=\"select($event, i)\" style=\"padding:3px 10px;cursor:pointer;\">\n" +
|
||||
" <i class=\"fa\" ng-class=\"{'fa-check': i.checked, 'empty': !i.checked}\"></i> {{i.label}}" +
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
AngularJS v1.5.8
|
||||
(c) 2010-2016 Google, Inc. http://angularjs.org
|
||||
License: MIT
|
||||
*/
|
||||
(function(n,c){'use strict';function l(b,a,g){var d=g.baseHref(),k=b[0];return function(b,e,f){var g,h;f=f||{};h=f.expires;g=c.isDefined(f.path)?f.path:d;c.isUndefined(e)&&(h="Thu, 01 Jan 1970 00:00:00 GMT",e="");c.isString(h)&&(h=new Date(h));e=encodeURIComponent(b)+"="+encodeURIComponent(e);e=e+(g?";path="+g:"")+(f.domain?";domain="+f.domain:"");e+=h?";expires="+h.toUTCString():"";e+=f.secure?";secure":"";f=e.length+1;4096<f&&a.warn("Cookie '"+b+"' possibly not set or overflowed because it was too large ("+
|
||||
f+" > 4096 bytes)!");k.cookie=e}}c.module("ngCookies",["ng"]).provider("$cookies",[function(){var b=this.defaults={};this.$get=["$$cookieReader","$$cookieWriter",function(a,g){return{get:function(d){return a()[d]},getObject:function(d){return(d=this.get(d))?c.fromJson(d):d},getAll:function(){return a()},put:function(d,a,m){g(d,a,m?c.extend({},b,m):b)},putObject:function(d,b,a){this.put(d,c.toJson(b),a)},remove:function(a,k){g(a,void 0,k?c.extend({},b,k):b)}}}]}]);c.module("ngCookies").factory("$cookieStore",
|
||||
["$cookies",function(b){return{get:function(a){return b.getObject(a)},put:function(a,c){b.putObject(a,c)},remove:function(a){b.remove(a)}}}]);l.$inject=["$document","$log","$browser"];c.module("ngCookies").provider("$$cookieWriter",function(){this.$get=l})})(window,window.angular);
|
||||
//# sourceMappingURL=angular-cookies.min.js.map
|
||||
@@ -0,0 +1,6 @@
|
||||
/*!
|
||||
* angular-translate - v2.18.3 - 2020-07-08
|
||||
*
|
||||
* Copyright (c) 2020 The angular-translate team, Pascal Precht; Licensed MIT
|
||||
*/
|
||||
!function(e,i){"function"==typeof define&&define.amd?define([],function(){return i()}):"object"==typeof module&&module.exports?module.exports=i():i()}(0,function(){function e(n,a){"use strict";return function(r){if(!(r&&(angular.isArray(r.files)||angular.isString(r.prefix)&&angular.isString(r.suffix))))throw new Error("Couldn't load static files, no files and prefix or suffix specified!");r.files||(r.files=[{prefix:r.prefix,suffix:r.suffix}]);for(var e=function(e){if(!e||!angular.isString(e.prefix)||!angular.isString(e.suffix))throw new Error("Couldn't load static file, no prefix or suffix specified!");var i=[e.prefix,r.key,e.suffix].join("");return angular.isObject(r.fileMap)&&r.fileMap[i]&&(i=r.fileMap[i]),a(angular.extend({url:i,method:"GET"},r.$http)).then(function(e){return e.data},function(){return n.reject(r.key)})},i=[],t=r.files.length,f=0;f<t;f++)i.push(e({prefix:r.files[f].prefix,key:r.key,suffix:r.files[f].suffix}));return n.all(i).then(function(e){for(var i=e.length,r={},t=0;t<i;t++)for(var f in e[t])r[f]=e[t][f];return r})}}return e.$inject=["$q","$http"],angular.module("pascalprecht.translate").factory("$translateStaticFilesLoader",e),e.displayName="$translateStaticFilesLoader","pascalprecht.translate"});
|
||||
@@ -0,0 +1,6 @@
|
||||
/*!
|
||||
* angular-translate - v2.18.3 - 2020-07-08
|
||||
*
|
||||
* Copyright (c) 2020 The angular-translate team, Pascal Precht; Licensed MIT
|
||||
*/
|
||||
!function(t,e){"function"==typeof define&&define.amd?define([],function(){return e()}):"object"==typeof module&&module.exports?module.exports=e():e()}(0,function(){function t(t){"use strict";var n;if(1===angular.version.major&&4<=angular.version.minor){var o=t.get("$cookies");n={get:function(t){return o.get(t)},put:function(t,e){o.put(t,e)}}}else{var r=t.get("$cookieStore");n={get:function(t){return r.get(t)},put:function(t,e){r.put(t,e)}}}return{get:function(t){return n.get(t)},set:function(t,e){n.put(t,e)},put:function(t,e){n.put(t,e)}}}return t.$inject=["$injector"],angular.module("pascalprecht.translate").factory("$translateCookieStorage",t),t.displayName="$translateCookieStorage","pascalprecht.translate"});
|
||||
@@ -0,0 +1,6 @@
|
||||
/*!
|
||||
* angular-translate - v2.18.3 - 2020-07-08
|
||||
*
|
||||
* Copyright (c) 2020 The angular-translate team, Pascal Precht; Licensed MIT
|
||||
*/
|
||||
!function(t,e){"function"==typeof define&&define.amd?define([],function(){return e()}):"object"==typeof module&&module.exports?module.exports=e():e()}(0,function(){function t(a,t){"use strict";var o,e={get:function(t){return o||(o=a.localStorage.getItem(t)),o},set:function(t,e){o=e,a.localStorage.setItem(t,e)},put:function(t,e){o=e,a.localStorage.setItem(t,e)}},r="localStorage"in a;if(r){var n="pascalprecht.translate.storageTest";try{r=null!==a.localStorage&&(a.localStorage.setItem(n,"foo"),a.localStorage.removeItem(n),!0)}catch(t){r=!1}}return r?e:t}return t.$inject=["$window","$translateCookieStorage"],angular.module("pascalprecht.translate").factory("$translateLocalStorage",t),t.displayName="$translateLocalStorageFactory","pascalprecht.translate"});
|
||||
+6
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+639
@@ -0,0 +1,639 @@
|
||||
(function($, angular) {
|
||||
|
||||
// eslint-disable-next-line angular/file-name, angular/no-service-method
|
||||
angular.module('ui.bootstrap.contextMenu', [])
|
||||
.service('CustomService', function () {
|
||||
'use strict';
|
||||
|
||||
return {
|
||||
initialize: function (item) {
|
||||
console.log('got here', item);
|
||||
}
|
||||
};
|
||||
|
||||
})
|
||||
.constant('ContextMenuEvents', {
|
||||
// Triggers when all the context menus have been closed
|
||||
ContextMenuAllClosed: 'context-menu-all-closed',
|
||||
// Triggers when any single conext menu is called.
|
||||
// Closing all context menus triggers this for each level open
|
||||
ContextMenuClosed: 'context-menu-closed',
|
||||
// Triggers right before the very first context menu is opened
|
||||
ContextMenuOpening: 'context-menu-opening',
|
||||
// Triggers right after any context menu is opened
|
||||
ContextMenuOpened: 'context-menu-opened'
|
||||
})
|
||||
.directive('contextMenu', ['$rootScope', 'ContextMenuEvents', '$parse', '$q', 'CustomService', '$sce', '$document', '$window', '$compile',
|
||||
function ($rootScope, ContextMenuEvents, $parse, $q, custom, $sce, $document, $window, $compile) {
|
||||
|
||||
var _contextMenus = [];
|
||||
// Contains the element that was clicked to show the context menu
|
||||
var _clickedElement = null;
|
||||
var DEFAULT_ITEM_TEXT = '"New Item';
|
||||
var _emptyText = 'empty';
|
||||
|
||||
function createAndAddOptionText(params) {
|
||||
// Destructuring:
|
||||
var $scope = params.$scope;
|
||||
var item = params.item;
|
||||
var event = params.event;
|
||||
var modelValue = params.modelValue;
|
||||
var $promises = params.$promises;
|
||||
var nestedMenu = params.nestedMenu;
|
||||
var $li = params.$li;
|
||||
var leftOriented = String(params.orientation).toLowerCase() === 'left';
|
||||
|
||||
var optionText = null;
|
||||
|
||||
if (item.html) {
|
||||
if (angular.isFunction(item.html)) {
|
||||
// runs the function that expects a jQuery/jqLite element
|
||||
optionText = item.html($scope);
|
||||
} else {
|
||||
// Incase we want to compile html string to initialize their custom directive in html string
|
||||
if (item.compile) {
|
||||
optionText = $compile(item.html)($scope);
|
||||
} else {
|
||||
// Assumes that the developer already placed a valid jQuery/jqLite element
|
||||
optionText = item.html;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
var $a = $('<a>');
|
||||
var $anchorStyle = {};
|
||||
|
||||
if (leftOriented) {
|
||||
$anchorStyle.textAlign = 'right';
|
||||
$anchorStyle.paddingLeft = '8px';
|
||||
} else {
|
||||
$anchorStyle.textAlign = 'left';
|
||||
$anchorStyle.paddingRight = '8px';
|
||||
}
|
||||
|
||||
$a.css($anchorStyle);
|
||||
$a.addClass('dropdown-item');
|
||||
$a.attr({ tabindex: '-1', href: '#' });
|
||||
|
||||
var textParam = item.text || item[0];
|
||||
var text = DEFAULT_ITEM_TEXT;
|
||||
|
||||
if (typeof textParam === 'string') {
|
||||
text = textParam;
|
||||
} else if (typeof textParam === 'function') {
|
||||
text = textParam.call($scope, $scope, event, modelValue);
|
||||
}
|
||||
|
||||
var $promise = $q.when(text);
|
||||
$promises.push($promise);
|
||||
$promise.then(function (pText) {
|
||||
if (nestedMenu) {
|
||||
var $arrow;
|
||||
var $boldStyle = {
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: 'bold'
|
||||
};
|
||||
|
||||
if (leftOriented) {
|
||||
$arrow = '<';
|
||||
$boldStyle.float = 'left';
|
||||
} else {
|
||||
$arrow = '>';
|
||||
$boldStyle.float = 'right';
|
||||
}
|
||||
|
||||
var $bold = $('<strong style="font-family:monospace;font-weight:bold;float:right;">' + $arrow + '</strong>');
|
||||
$bold.css($boldStyle);
|
||||
$a.css('cursor', 'default');
|
||||
$a.append($bold);
|
||||
}
|
||||
$a.append(pText);
|
||||
});
|
||||
|
||||
optionText = $a;
|
||||
}
|
||||
|
||||
$li.append(optionText);
|
||||
|
||||
return optionText;
|
||||
};
|
||||
|
||||
/**
|
||||
* Process each individual item
|
||||
*
|
||||
* Properties of params:
|
||||
* - $scope
|
||||
* - event
|
||||
* - modelValue
|
||||
* - level
|
||||
* - item
|
||||
* - $ul
|
||||
* - $li
|
||||
* - $promises
|
||||
*/
|
||||
function processItem(params) {
|
||||
var nestedMenu = extractNestedMenu(params);
|
||||
|
||||
// if html property is not defined, fallback to text, otherwise use default text
|
||||
// if first item in the item array is a function then invoke .call()
|
||||
// if first item is a string, then text should be the string.
|
||||
|
||||
var text = DEFAULT_ITEM_TEXT;
|
||||
var currItemParam = angular.extend({}, params);
|
||||
var item = params.item;
|
||||
var enabled = item.enabled === undefined ? item[2] : item.enabled;
|
||||
|
||||
currItemParam.nestedMenu = nestedMenu;
|
||||
currItemParam.enabled = resolveBoolOrFunc(enabled, params);
|
||||
currItemParam.text = createAndAddOptionText(currItemParam);
|
||||
|
||||
registerCurrentItemEvents(currItemParam);
|
||||
|
||||
};
|
||||
|
||||
/*
|
||||
* Registers the appropriate mouse events for options if the item is enabled.
|
||||
* Otherwise, it ensures that clicks to the item do not propagate.
|
||||
*/
|
||||
function registerCurrentItemEvents (params) {
|
||||
// Destructuring:
|
||||
var item = params.item;
|
||||
var $ul = params.$ul;
|
||||
var $li = params.$li;
|
||||
var $scope = params.$scope;
|
||||
var modelValue = params.modelValue;
|
||||
var level = params.level;
|
||||
var event = params.event;
|
||||
var text = params.text;
|
||||
var nestedMenu = params.nestedMenu;
|
||||
var enabled = params.enabled;
|
||||
var orientation = String(params.orientation).toLowerCase();
|
||||
var customClass = params.customClass;
|
||||
|
||||
if (enabled) {
|
||||
var openNestedMenu = function ($event) {
|
||||
removeContextMenus(level + 1);
|
||||
/*
|
||||
* The object here needs to be constructed and filled with data
|
||||
* on an "as needed" basis. Copying the data from event directly
|
||||
* or cloning the event results in unpredictable behavior.
|
||||
*/
|
||||
/// adding the original event in the object to use the attributes of the mouse over event in the promises
|
||||
var ev = {
|
||||
pageX: orientation === 'left' ? event.pageX - $ul[0].offsetWidth + 1 : event.pageX + $ul[0].offsetWidth - 1,
|
||||
pageY: $ul[0].offsetTop + $li[0].offsetTop - 3,
|
||||
// eslint-disable-next-line angular/window-service
|
||||
view: event.view || window,
|
||||
target: event.target,
|
||||
event: $event
|
||||
};
|
||||
|
||||
/*
|
||||
* At this point, nestedMenu can only either be an Array or a promise.
|
||||
* Regardless, passing them to `when` makes the implementation singular.
|
||||
*/
|
||||
$q.when(nestedMenu).then(function(promisedNestedMenu) {
|
||||
if (angular.isFunction(promisedNestedMenu)) {
|
||||
// support for dynamic subitems
|
||||
promisedNestedMenu = promisedNestedMenu.call($scope, $scope, event, modelValue, text, $li);
|
||||
}
|
||||
var nestedParam = {
|
||||
$scope : $scope,
|
||||
event : ev,
|
||||
options : promisedNestedMenu,
|
||||
modelValue : modelValue,
|
||||
level : level + 1,
|
||||
orientation: orientation,
|
||||
customClass: customClass
|
||||
};
|
||||
renderContextMenu(nestedParam);
|
||||
});
|
||||
};
|
||||
|
||||
$li.on('click', function ($event) {
|
||||
if($event.which == 1) {
|
||||
$event.preventDefault();
|
||||
$scope.$apply(function () {
|
||||
|
||||
var cleanupFunction = function () {
|
||||
$(event.currentTarget).removeClass('context');
|
||||
removeAllContextMenus();
|
||||
};
|
||||
var clickFunction = angular.isFunction(item.click)
|
||||
? item.click
|
||||
: (angular.isFunction(item[1])
|
||||
? item[1]
|
||||
: null);
|
||||
|
||||
if (clickFunction) {
|
||||
var res = clickFunction.call($scope, $scope, event, modelValue, text, $li);
|
||||
if(res === undefined || res) {
|
||||
cleanupFunction();
|
||||
}
|
||||
} else {
|
||||
cleanupFunction();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$li.on('mouseover', function ($event) {
|
||||
$scope.$apply(function () {
|
||||
if (nestedMenu) {
|
||||
openNestedMenu($event);
|
||||
} else {
|
||||
removeContextMenus(level + 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
setElementDisabled($li);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param params - an object containing the `item` parameter
|
||||
* @returns an Array or a Promise containing the children,
|
||||
* or null if the option has no submenu
|
||||
*/
|
||||
function extractNestedMenu(params) {
|
||||
// Destructuring:
|
||||
var item = params.item;
|
||||
|
||||
// New implementation:
|
||||
if (item.children) {
|
||||
if (angular.isFunction(item.children)) {
|
||||
// Expects a function that returns a Promise or an Array
|
||||
return item.children();
|
||||
} else if (angular.isFunction(item.children.then) || angular.isArray(item.children)) {
|
||||
// Returns the promise
|
||||
// OR, returns the actual array
|
||||
return item.children;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
} else {
|
||||
// nestedMenu is either an Array or a Promise that will return that array.
|
||||
// NOTE: This might be changed soon as it's a hangover from an old implementation
|
||||
|
||||
return angular.isArray(item[1]) ||
|
||||
(item[1] && angular.isFunction(item[1].then)) ? item[1] : angular.isArray(item[2]) ||
|
||||
(item[2] && angular.isFunction(item[2].then)) ? item[2] : angular.isArray(item[3]) ||
|
||||
(item[3] && angular.isFunction(item[3].then)) ? item[3] : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for the actual rendering of the context menu.
|
||||
*
|
||||
* The parameters in params are:
|
||||
* - $scope = the scope of this context menu
|
||||
* - event = the event that triggered this context menu
|
||||
* - options = the options for this context menu
|
||||
* - modelValue = the value of the model attached to this context menu
|
||||
* - level = the current context menu level (defauts to 0)
|
||||
* - customClass = the custom class to be used for the context menu
|
||||
*/
|
||||
function renderContextMenu (params) {
|
||||
/// <summary>Render context menu recursively.</summary>
|
||||
|
||||
// Destructuring:
|
||||
var $scope = params.$scope;
|
||||
var event = params.event;
|
||||
var options = params.options;
|
||||
var modelValue = params.modelValue;
|
||||
var level = params.level;
|
||||
var customClass = params.customClass;
|
||||
|
||||
// Initialize the container. This will be passed around
|
||||
var $ul = initContextMenuContainer(params);
|
||||
params.$ul = $ul;
|
||||
|
||||
// Register this level of the context menu
|
||||
_contextMenus.push($ul);
|
||||
|
||||
/*
|
||||
* This object will contain any promises that we have
|
||||
* to wait for before trying to adjust the context menu.
|
||||
*/
|
||||
var $promises = [];
|
||||
params.$promises = $promises;
|
||||
|
||||
angular.forEach(options, function (item) {
|
||||
|
||||
if (item === null) {
|
||||
appendDivider($ul);
|
||||
} else {
|
||||
// If displayed is anything other than a function or a boolean
|
||||
var displayed = resolveBoolOrFunc(item.displayed, params);
|
||||
|
||||
// Only add the <li> if the item is displayed
|
||||
if (displayed) {
|
||||
var $li = $('<li>');
|
||||
var itemParams = angular.extend({}, params);
|
||||
itemParams.item = item;
|
||||
itemParams.$li = $li;
|
||||
|
||||
if (typeof item[0] === 'object') {
|
||||
custom.initialize($li, item);
|
||||
} else {
|
||||
processItem(itemParams);
|
||||
}
|
||||
if (resolveBoolOrFunc(item.hasTopDivider, itemParams, false)) {
|
||||
appendDivider($ul);
|
||||
}
|
||||
$ul.append($li);
|
||||
if (resolveBoolOrFunc(item.hasBottomDivider, itemParams, false)) {
|
||||
appendDivider($ul);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if ($ul.children().length === 0) {
|
||||
var $emptyLi = angular.element('<li>');
|
||||
setElementDisabled($emptyLi);
|
||||
$emptyLi.html('<a>' + _emptyText + '</a>');
|
||||
$ul.append($emptyLi);
|
||||
}
|
||||
|
||||
$document.find('body').append($ul);
|
||||
|
||||
doAfterAllPromises(params);
|
||||
|
||||
$rootScope.$broadcast(ContextMenuEvents.ContextMenuOpened, {
|
||||
context: _clickedElement,
|
||||
contextMenu: $ul,
|
||||
params: params
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* calculate if drop down menu would go out of screen at left or bottom
|
||||
* calculation need to be done after element has been added (and all texts are set; thus the promises)
|
||||
* to the DOM the get the actual height
|
||||
*/
|
||||
function doAfterAllPromises (params) {
|
||||
|
||||
// Desctructuring:
|
||||
var $ul = params.$ul;
|
||||
var $promises = params.$promises;
|
||||
var level = params.level;
|
||||
var event = params.event;
|
||||
var leftOriented = String(params.orientation).toLowerCase() === 'left';
|
||||
|
||||
$q.all($promises).then(function () {
|
||||
var topCoordinate = event.pageY;
|
||||
var menuHeight = angular.element($ul[0]).prop('offsetHeight');
|
||||
var winHeight = $window.pageYOffset + event.view.innerHeight;
|
||||
|
||||
/// the 20 pixels in second condition are considering the browser status bar that sometimes overrides the element
|
||||
if (topCoordinate > menuHeight && winHeight - topCoordinate < menuHeight + 20) {
|
||||
topCoordinate = event.pageY - menuHeight;
|
||||
/// If the element is a nested menu, adds the height of the parent li to the topCoordinate to align with the parent
|
||||
if(level && level > 0) {
|
||||
topCoordinate += event.event.currentTarget.offsetHeight;
|
||||
}
|
||||
} else if(winHeight <= menuHeight) {
|
||||
// If it really can't fit, reset the height of the menu to one that will fit
|
||||
angular.element($ul[0]).css({ 'height': winHeight - 5, 'overflow-y': 'scroll' });
|
||||
// ...then set the topCoordinate height to 0 so the menu starts from the top
|
||||
topCoordinate = 0;
|
||||
} else if(winHeight - topCoordinate < menuHeight) {
|
||||
var reduceThresholdY = 5;
|
||||
if(topCoordinate < reduceThresholdY) {
|
||||
reduceThresholdY = topCoordinate;
|
||||
}
|
||||
topCoordinate = winHeight - menuHeight - reduceThresholdY;
|
||||
}
|
||||
|
||||
var leftCoordinate = event.pageX;
|
||||
var menuWidth = angular.element($ul[0]).prop('offsetWidth');
|
||||
var winWidth = event.view.innerWidth + window.pageXOffset;
|
||||
var padding = 5;
|
||||
|
||||
if (leftOriented) {
|
||||
if (winWidth - leftCoordinate > menuWidth && leftCoordinate < menuWidth + padding) {
|
||||
leftCoordinate = padding;
|
||||
} else if (leftCoordinate < menuWidth) {
|
||||
var reduceThresholdX = 5;
|
||||
if (winWidth - leftCoordinate < reduceThresholdX + padding) {
|
||||
reduceThresholdX = winWidth - leftCoordinate + padding;
|
||||
}
|
||||
leftCoordinate = menuWidth + reduceThresholdX + padding;
|
||||
} else {
|
||||
leftCoordinate = leftCoordinate - menuWidth;
|
||||
}
|
||||
} else {
|
||||
if (leftCoordinate > menuWidth && winWidth - leftCoordinate - padding < menuWidth) {
|
||||
leftCoordinate = winWidth - menuWidth - padding;
|
||||
} else if(winWidth - leftCoordinate < menuWidth) {
|
||||
var reduceThresholdX = 5;
|
||||
if(leftCoordinate < reduceThresholdX + padding) {
|
||||
reduceThresholdX = leftCoordinate + padding;
|
||||
}
|
||||
leftCoordinate = winWidth - menuWidth - reduceThresholdX - padding;
|
||||
}
|
||||
}
|
||||
|
||||
$ul.css({
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
left: leftCoordinate + 'px',
|
||||
top: topCoordinate + 'px'
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the container of the context menu (a <ul> element),
|
||||
* applies the appropriate styles and then returns that container
|
||||
*
|
||||
* @return a <ul> jqLite/jQuery element
|
||||
*/
|
||||
function initContextMenuContainer(params) {
|
||||
// Destructuring
|
||||
var customClass = params.customClass;
|
||||
|
||||
var $ul = $('<ul>');
|
||||
$ul.addClass('dropdown-menu');
|
||||
$ul.attr({ 'role': 'menu' });
|
||||
$ul.css({
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
left: params.event.pageX + 'px',
|
||||
top: params.event.pageY + 'px',
|
||||
'z-index': 10000
|
||||
});
|
||||
|
||||
if(customClass) { $ul.addClass(customClass); }
|
||||
|
||||
return $ul;
|
||||
}
|
||||
|
||||
function isTouchDevice() {
|
||||
return 'ontouchstart' in window || navigator.maxTouchPoints; // works on most browsers | works on IE10/11 and Surface
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the context menus with level greater than or equal
|
||||
* to the value passed. If undefined, null or 0, all context menus
|
||||
* are removed.
|
||||
*/
|
||||
function removeContextMenus (level) {
|
||||
while (_contextMenus.length && (!level || _contextMenus.length > level)) {
|
||||
var cm = _contextMenus.pop();
|
||||
$rootScope.$broadcast(ContextMenuEvents.ContextMenuClosed, { context: _clickedElement, contextMenu: cm });
|
||||
cm.remove();
|
||||
}
|
||||
if(!level) {
|
||||
$rootScope.$broadcast(ContextMenuEvents.ContextMenuAllClosed, { context: _clickedElement });
|
||||
}
|
||||
}
|
||||
|
||||
function removeOnScrollEvent(e) {
|
||||
removeAllContextMenus(e);
|
||||
}
|
||||
|
||||
function removeOnOutsideClickEvent(e) {
|
||||
|
||||
var $curr = $(e.target);
|
||||
var shouldRemove = true;
|
||||
|
||||
while($curr.length) {
|
||||
if ($curr.hasClass('dropdown-menu')) {
|
||||
shouldRemove = false;
|
||||
break;
|
||||
} else {
|
||||
$curr = $curr.parent();
|
||||
}
|
||||
}
|
||||
if (shouldRemove) {
|
||||
removeAllContextMenus(e);
|
||||
}
|
||||
}
|
||||
|
||||
function removeAllContextMenus(e) {
|
||||
$document.find('body').off('mousedown touchstart', removeOnOutsideClickEvent);
|
||||
$document.off('scroll', removeOnScrollEvent);
|
||||
$(_clickedElement).removeClass('context');
|
||||
removeContextMenus();
|
||||
$rootScope.$broadcast('');
|
||||
}
|
||||
|
||||
function isBoolean(a) {
|
||||
return a === false || a === true;
|
||||
}
|
||||
|
||||
/** Resolves a boolean or a function that returns a boolean
|
||||
* Returns true by default if the param is null or undefined
|
||||
* @param a - the parameter to be checked
|
||||
* @param params - the object for the item's parameters
|
||||
* @param defaultValue - the default boolean value to use if the parameter is
|
||||
* neither a boolean nor function. True by default.
|
||||
*/
|
||||
function resolveBoolOrFunc(a, params, defaultValue) {
|
||||
var item = params.item;
|
||||
var $scope = params.$scope;
|
||||
var event = params.event;
|
||||
var modelValue = params.modelValue;
|
||||
|
||||
defaultValue = isBoolean(defaultValue) ? defaultValue : true;
|
||||
|
||||
if (isBoolean(a)) {
|
||||
return a;
|
||||
} else if (angular.isFunction(a)) {
|
||||
return a.call($scope, $scope, event, modelValue);
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
function appendDivider($ul) {
|
||||
var $li = angular.element('<li>');
|
||||
$li.addClass('divider');
|
||||
$ul.append($li);
|
||||
}
|
||||
|
||||
function setElementDisabled($li) {
|
||||
$li.on('click', function ($event) {
|
||||
$event.preventDefault();
|
||||
});
|
||||
$li.addClass('disabled');
|
||||
}
|
||||
|
||||
return function ($scope, element, attrs) {
|
||||
var openMenuEvents = ['contextmenu'];
|
||||
_emptyText = $scope.$eval(attrs.contextMenuEmptyText) || 'empty';
|
||||
|
||||
if(attrs.contextMenuOn && typeof(attrs.contextMenuOn) === 'string'){
|
||||
openMenuEvents = attrs.contextMenuOn.split(',');
|
||||
}
|
||||
|
||||
angular.forEach(openMenuEvents, function (openMenuEvent) {
|
||||
element.on(openMenuEvent.trim(), function (event) {
|
||||
// Cleanup any leftover contextmenus(there are cases with longpress on touch where we
|
||||
// still see multiple contextmenus)
|
||||
removeAllContextMenus();
|
||||
|
||||
if(!attrs.allowEventPropagation) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// Don't show context menu if on touch device and element is draggable
|
||||
if(isTouchDevice() && element.attr('draggable') === 'true') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove if the user clicks outside
|
||||
$document.find('body').on('mousedown touchstart', removeOnOutsideClickEvent);
|
||||
// Remove the menu when the scroll moves
|
||||
$document.on('scroll', removeOnScrollEvent);
|
||||
|
||||
_clickedElement = event.currentTarget;
|
||||
$(_clickedElement).addClass('context');
|
||||
|
||||
$scope.$apply(function () {
|
||||
var options = $scope.$eval(attrs.contextMenu);
|
||||
var customClass = attrs.contextMenuClass;
|
||||
var modelValue = $scope.$eval(attrs.model);
|
||||
var orientation = attrs.contextMenuOrientation;
|
||||
|
||||
$q.when(options).then(function(promisedMenu) {
|
||||
if (angular.isFunction(promisedMenu)) {
|
||||
// support for dynamic items
|
||||
promisedMenu = promisedMenu.call($scope, $scope, event, modelValue);
|
||||
}
|
||||
var params = {
|
||||
'$scope' : $scope,
|
||||
'event' : event,
|
||||
'options' : promisedMenu,
|
||||
'modelValue' : modelValue,
|
||||
'level' : 0,
|
||||
'customClass' : customClass,
|
||||
'orientation': orientation
|
||||
};
|
||||
$rootScope.$broadcast(ContextMenuEvents.ContextMenuOpening, { context: _clickedElement });
|
||||
renderContextMenu(params);
|
||||
});
|
||||
});
|
||||
|
||||
// Remove all context menus if the scope is destroyed
|
||||
$scope.$on('$destroy', function () {
|
||||
removeAllContextMenus();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (attrs.closeMenuOn) {
|
||||
$scope.$on(attrs.closeMenuOn, function () {
|
||||
removeAllContextMenus();
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
// eslint-disable-next-line angular/window-service
|
||||
})(window.angular.element, window.angular);
|
||||
Vendored
+10
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
-5
File diff suppressed because one or more lines are too long
+3
File diff suppressed because one or more lines are too long
@@ -1,4 +0,0 @@
|
||||
/*! showdown-target-blank 02-11-2015 */
|
||||
|
||||
!function(){"use strict";var a=function(){return[{type:"output",regex:"<a(.*?)>",replace:function(a,b){return'<a target="_blank"'+b+">"}}]};"undefined"!=typeof window&&window.showdown&&window.showdown.extensions&&window.showdown.extension("targetblank",a),"undefined"!=typeof module&&(module.exports=a)}();
|
||||
//# sourceMappingURL=showdown-target-blank.min.js.map
|
||||
-130
@@ -1,130 +0,0 @@
|
||||
/**
|
||||
* Implements the attach method, that attaches the terminal to a WebSocket stream.
|
||||
* @module xterm/addons/attach/attach
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
(function (attach) {
|
||||
if (typeof exports === 'object' && typeof module === 'object') {
|
||||
/*
|
||||
* CommonJS environment
|
||||
*/
|
||||
module.exports = attach(require('../../xterm'));
|
||||
} else if (typeof define == 'function') {
|
||||
/*
|
||||
* Require.js is available
|
||||
*/
|
||||
define(['../../xterm'], attach);
|
||||
} else {
|
||||
/*
|
||||
* Plain browser environment
|
||||
*/
|
||||
attach(window.Terminal);
|
||||
}
|
||||
})(function (Xterm) {
|
||||
'use strict';
|
||||
|
||||
var exports = {};
|
||||
|
||||
/**
|
||||
* Attaches the given terminal to the given socket.
|
||||
*
|
||||
* @param {Xterm} term - The terminal to be attached to the given socket.
|
||||
* @param {WebSocket} socket - The socket to attach the current terminal.
|
||||
* @param {boolean} bidirectional - Whether the terminal should send data
|
||||
* to the socket as well.
|
||||
* @param {boolean} buffered - Whether the rendering of incoming data
|
||||
* should happen instantly or at a maximum
|
||||
* frequency of 1 rendering per 10ms.
|
||||
*/
|
||||
exports.attach = function (term, socket, bidirectional, buffered) {
|
||||
bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
|
||||
term.socket = socket;
|
||||
|
||||
term._flushBuffer = function () {
|
||||
term.write(term._attachSocketBuffer);
|
||||
term._attachSocketBuffer = null;
|
||||
clearTimeout(term._attachSocketBufferTimer);
|
||||
term._attachSocketBufferTimer = null;
|
||||
};
|
||||
|
||||
term._pushToBuffer = function (data) {
|
||||
if (term._attachSocketBuffer) {
|
||||
term._attachSocketBuffer += data;
|
||||
} else {
|
||||
term._attachSocketBuffer = data;
|
||||
setTimeout(term._flushBuffer, 10);
|
||||
}
|
||||
};
|
||||
|
||||
term._getMessage = function (ev) {
|
||||
if (buffered) {
|
||||
term._pushToBuffer(ev.data);
|
||||
} else {
|
||||
term.write(ev.data);
|
||||
}
|
||||
};
|
||||
|
||||
term._sendData = function (data) {
|
||||
if (socket.readyState !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
socket.send(data);
|
||||
};
|
||||
|
||||
socket.addEventListener('message', term._getMessage);
|
||||
|
||||
if (bidirectional) {
|
||||
term.on('data', term._sendData);
|
||||
}
|
||||
|
||||
socket.addEventListener('close', term.detach.bind(term, socket));
|
||||
socket.addEventListener('error', term.detach.bind(term, socket));
|
||||
};
|
||||
|
||||
/**
|
||||
* Detaches the given terminal from the given socket
|
||||
*
|
||||
* @param {Xterm} term - The terminal to be detached from the given socket.
|
||||
* @param {WebSocket} socket - The socket from which to detach the current
|
||||
* terminal.
|
||||
*/
|
||||
exports.detach = function (term, socket) {
|
||||
term.off('data', term._sendData);
|
||||
|
||||
socket = (typeof socket == 'undefined') ? term.socket : socket;
|
||||
|
||||
if (socket) {
|
||||
socket.removeEventListener('message', term._getMessage);
|
||||
}
|
||||
|
||||
delete term.socket;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches the current terminal to the given socket
|
||||
*
|
||||
* @param {WebSocket} socket - The socket to attach the current terminal.
|
||||
* @param {boolean} bidirectional - Whether the terminal should send data
|
||||
* to the socket as well.
|
||||
* @param {boolean} buffered - Whether the rendering of incoming data
|
||||
* should happen instantly or at a maximum
|
||||
* frequency of 1 rendering per 10ms.
|
||||
*/
|
||||
Xterm.prototype.attach = function (socket, bidirectional, buffered) {
|
||||
return exports.attach(this, socket, bidirectional, buffered);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detaches the current terminal from the given socket.
|
||||
*
|
||||
* @param {WebSocket} socket - The socket from which to detach the current
|
||||
* terminal.
|
||||
*/
|
||||
Xterm.prototype.detach = function (socket) {
|
||||
return exports.detach(this, socket);
|
||||
};
|
||||
|
||||
return exports;
|
||||
});
|
||||
Vendored
-86
@@ -1,86 +0,0 @@
|
||||
/**
|
||||
* Fit terminal columns and rows to the dimensions of its DOM element.
|
||||
*
|
||||
* ## Approach
|
||||
* - Rows: Truncate the division of the terminal parent element height by the terminal row height.
|
||||
*
|
||||
* - Columns: Truncate the division of the terminal parent element width by the terminal character
|
||||
* width (apply display: inline at the terminal row and truncate its width with the current
|
||||
* number of columns).
|
||||
* @module xterm/addons/fit/fit
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
(function (fit) {
|
||||
if (typeof exports === 'object' && typeof module === 'object') {
|
||||
/*
|
||||
* CommonJS environment
|
||||
*/
|
||||
module.exports = fit(require('../../xterm'));
|
||||
} else if (typeof define == 'function') {
|
||||
/*
|
||||
* Require.js is available
|
||||
*/
|
||||
define(['../../xterm'], fit);
|
||||
} else {
|
||||
/*
|
||||
* Plain browser environment
|
||||
*/
|
||||
fit(window.Terminal);
|
||||
}
|
||||
})(function (Xterm) {
|
||||
var exports = {};
|
||||
|
||||
exports.proposeGeometry = function (term) {
|
||||
if (!term.element.parentElement) {
|
||||
return null;
|
||||
}
|
||||
var parentElementStyle = window.getComputedStyle(term.element.parentElement),
|
||||
parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')),
|
||||
parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17),
|
||||
elementStyle = window.getComputedStyle(term.element),
|
||||
elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom')),
|
||||
elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left')),
|
||||
availableHeight = parentElementHeight - elementPaddingVer,
|
||||
availableWidth = parentElementWidth - elementPaddingHor,
|
||||
container = term.rowContainer,
|
||||
subjectRow = term.rowContainer.firstElementChild,
|
||||
contentBuffer = subjectRow.innerHTML,
|
||||
characterHeight,
|
||||
rows,
|
||||
characterWidth,
|
||||
cols,
|
||||
geometry;
|
||||
|
||||
subjectRow.style.display = 'inline';
|
||||
subjectRow.innerHTML = 'W'; // Common character for measuring width, although on monospace
|
||||
characterWidth = subjectRow.getBoundingClientRect().width;
|
||||
subjectRow.style.display = ''; // Revert style before calculating height, since they differ.
|
||||
characterHeight = subjectRow.getBoundingClientRect().height;
|
||||
subjectRow.innerHTML = contentBuffer;
|
||||
|
||||
rows = parseInt(availableHeight / characterHeight);
|
||||
cols = parseInt(availableWidth / characterWidth);
|
||||
|
||||
geometry = {cols: cols, rows: rows};
|
||||
return geometry;
|
||||
};
|
||||
|
||||
exports.fit = function (term) {
|
||||
var geometry = exports.proposeGeometry(term);
|
||||
|
||||
if (geometry) {
|
||||
term.resize(geometry.cols, geometry.rows);
|
||||
}
|
||||
};
|
||||
|
||||
Xterm.prototype.proposeGeometry = function () {
|
||||
return exports.proposeGeometry(this);
|
||||
};
|
||||
|
||||
Xterm.prototype.fit = function () {
|
||||
return exports.fit(this);
|
||||
};
|
||||
|
||||
return exports;
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
.xterm.fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: auto;
|
||||
height: auto;
|
||||
z-index: 255;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/**
|
||||
* Fullscreen addon for xterm.js
|
||||
* @module xterm/addons/fullscreen/fullscreen
|
||||
* @license MIT
|
||||
*/
|
||||
(function (fullscreen) {
|
||||
if (typeof exports === 'object' && typeof module === 'object') {
|
||||
/*
|
||||
* CommonJS environment
|
||||
*/
|
||||
module.exports = fullscreen(require('../../xterm'));
|
||||
} else if (typeof define == 'function') {
|
||||
/*
|
||||
* Require.js is available
|
||||
*/
|
||||
define(['../../xterm'], fullscreen);
|
||||
} else {
|
||||
/*
|
||||
* Plain browser environment
|
||||
*/
|
||||
fullscreen(window.Terminal);
|
||||
}
|
||||
})(function (Xterm) {
|
||||
var exports = {};
|
||||
|
||||
/**
|
||||
* Toggle the given terminal's fullscreen mode.
|
||||
* @param {Xterm} term - The terminal to toggle full screen mode
|
||||
* @param {boolean} fullscreen - Toggle fullscreen on (true) or off (false)
|
||||
*/
|
||||
exports.toggleFullScreen = function (term, fullscreen) {
|
||||
var fn;
|
||||
|
||||
if (typeof fullscreen == 'undefined') {
|
||||
fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add';
|
||||
} else if (!fullscreen) {
|
||||
fn = 'remove';
|
||||
} else {
|
||||
fn = 'add';
|
||||
}
|
||||
|
||||
term.element.classList[fn]('fullscreen');
|
||||
};
|
||||
|
||||
Xterm.prototype.toggleFullscreen = function (fullscreen) {
|
||||
exports.toggleFullScreen(this, fullscreen);
|
||||
};
|
||||
|
||||
return exports;
|
||||
});
|
||||
-207
@@ -1,207 +0,0 @@
|
||||
/**
|
||||
* Methods for turning URL subscrings in the terminal's content into links (`a` DOM elements).
|
||||
* @module xterm/addons/linkify/linkify
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
(function (linkify) {
|
||||
if (typeof exports === 'object' && typeof module === 'object') {
|
||||
/*
|
||||
* CommonJS environment
|
||||
*/
|
||||
module.exports = linkify(require('../../xterm'));
|
||||
} else if (typeof define == 'function') {
|
||||
/*
|
||||
* Require.js is available
|
||||
*/
|
||||
define(['../../xterm'], linkify);
|
||||
} else {
|
||||
/*
|
||||
* Plain browser environment
|
||||
*/
|
||||
linkify(window.Terminal);
|
||||
}
|
||||
})(function (Xterm) {
|
||||
'use strict';
|
||||
|
||||
var exports = {},
|
||||
protocolClause = '(https?:\\/\\/)',
|
||||
domainCharacterSet = '[\\da-z\\.-]+',
|
||||
negatedDomainCharacterSet = '[^\\da-z\\.-]+',
|
||||
domainBodyClause = '(' + domainCharacterSet + ')',
|
||||
tldClause = '([a-z\\.]{2,6})',
|
||||
ipClause = '((\\d{1,3}\\.){3}\\d{1,3})',
|
||||
portClause = '(:\\d{1,5})',
|
||||
hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + ')' + portClause + '?',
|
||||
pathClause = '(\\/[\\/\\w\\.-]*)*',
|
||||
negatedPathCharacterSet = '[^\\/\\w\\.-]+',
|
||||
bodyClause = hostClause + pathClause,
|
||||
start = '(?:^|' + negatedDomainCharacterSet + ')(',
|
||||
end = ')($|' + negatedPathCharacterSet + ')',
|
||||
lenientUrlClause = start + protocolClause + '?' + bodyClause + end,
|
||||
strictUrlClause = start + protocolClause + bodyClause + end,
|
||||
lenientUrlRegex = new RegExp(lenientUrlClause),
|
||||
strictUrlRegex = new RegExp(strictUrlClause);
|
||||
|
||||
/**
|
||||
* Converts all valid URLs found in the given terminal line into
|
||||
* hyperlinks. The terminal line can be either the HTML element itself
|
||||
* or the index of the termina line in the children of the terminal
|
||||
* rows container.
|
||||
*
|
||||
* @param {Xterm} terminal - The terminal that owns the given line.
|
||||
* @param {number|HTMLDivElement} line - The terminal line that should get
|
||||
* "linkified".
|
||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
||||
* false, the regex requires a protocol clause. Defaults to true.
|
||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
||||
* Default doesn't set target attribute
|
||||
* @emits linkify
|
||||
* @emits linkify:line
|
||||
*/
|
||||
exports.linkifyTerminalLine = function (terminal, line, lenient, target) {
|
||||
if (typeof line == 'number') {
|
||||
line = terminal.rowContainer.children[line];
|
||||
} else if (! (line instanceof HTMLDivElement)) {
|
||||
var message = 'The "line" argument should be either a number';
|
||||
message += ' or an HTMLDivElement';
|
||||
|
||||
throw new TypeError(message);
|
||||
}
|
||||
|
||||
if (typeof target === 'undefined') {
|
||||
target = '';
|
||||
} else {
|
||||
target = 'target="' + target + '"';
|
||||
}
|
||||
|
||||
var buffer = document.createElement('span'),
|
||||
nodes = line.childNodes;
|
||||
|
||||
for (var j=0; j<nodes.length; j++) {
|
||||
var node = nodes[j],
|
||||
match;
|
||||
|
||||
/**
|
||||
* Since we cannot access the TextNode's HTML representation
|
||||
* from the instance itself, we assign its data as textContent
|
||||
* to a dummy buffer span, in order to retrieve the TextNode's
|
||||
* HTML representation from the buffer's innerHTML.
|
||||
*/
|
||||
buffer.textContent = node.data;
|
||||
|
||||
var nodeHTML = buffer.innerHTML;
|
||||
|
||||
/**
|
||||
* Apply function only on TextNodes
|
||||
*/
|
||||
if (node.nodeType != node.TEXT_NODE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var url = exports.findLinkMatch(node.data, lenient);
|
||||
|
||||
if (!url) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var startsWithProtocol = new RegExp('^' + protocolClause),
|
||||
urlHasProtocol = url.match(startsWithProtocol),
|
||||
href = (urlHasProtocol) ? url : 'http://' + url,
|
||||
link = '<a href="' + href + '" ' + target + '>' + url + '</a>',
|
||||
newHTML = nodeHTML.replace(url, link);
|
||||
|
||||
line.innerHTML = line.innerHTML.replace(nodeHTML, newHTML);
|
||||
}
|
||||
|
||||
/**
|
||||
* This event gets emitted when conversion of all URL susbtrings
|
||||
* to HTML anchor elements (links) has finished, for a specific
|
||||
* line of the current Xterm instance.
|
||||
*
|
||||
* @event linkify:line
|
||||
*/
|
||||
terminal.emit('linkify:line', line);
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds a link within a block of text.
|
||||
*
|
||||
* @param {string} text - The text to search .
|
||||
* @param {boolean} lenient - Whether to use the lenient search.
|
||||
* @return {string} A URL.
|
||||
*/
|
||||
exports.findLinkMatch = function (text, lenient) {
|
||||
var match = text.match(lenient ? lenientUrlRegex : strictUrlRegex);
|
||||
if (!match || match.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts all valid URLs found in the terminal view into hyperlinks.
|
||||
*
|
||||
* @param {Xterm} terminal - The terminal that should get "linkified".
|
||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
||||
* false, the regex requires a protocol clause. Defaults to true.
|
||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
||||
* Default doesn't set target attribute
|
||||
* @emits linkify
|
||||
* @emits linkify:line
|
||||
*/
|
||||
exports.linkify = function (terminal, lenient, target) {
|
||||
var rows = terminal.rowContainer.children;
|
||||
|
||||
lenient = (typeof lenient == "boolean") ? lenient : true;
|
||||
for (var i=0; i<rows.length; i++) {
|
||||
var line = rows[i];
|
||||
|
||||
exports.linkifyTerminalLine(terminal, line, lenient, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* This event gets emitted when conversion of all URL substrings to
|
||||
* HTML anchor elements (links) has finished for the current Xterm
|
||||
* instance's view.
|
||||
*
|
||||
* @event linkify
|
||||
*/
|
||||
terminal.emit('linkify');
|
||||
};
|
||||
|
||||
/**
|
||||
* Extend Xterm prototype.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Converts all valid URLs found in the current terminal linte into
|
||||
* hyperlinks.
|
||||
*
|
||||
* @memberof Xterm
|
||||
* @param {number|HTMLDivElement} line - The terminal line that should get
|
||||
* "linkified".
|
||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
||||
* false, the regex requires a protocol clause. Defaults to true.
|
||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
||||
* Default doesn't set target attribute
|
||||
*/
|
||||
Xterm.prototype.linkifyTerminalLine = function (line, lenient, target) {
|
||||
return exports.linkifyTerminalLine(this, line, lenient, target);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts all valid URLs found in the current terminal into hyperlinks.
|
||||
*
|
||||
* @memberof Xterm
|
||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
||||
* false, the regex requires a protocol clause. Defaults to true.
|
||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
||||
* Default doesn't set target attribute
|
||||
*/
|
||||
Xterm.prototype.linkify = function (lenient, target) {
|
||||
return exports.linkify(this, lenient, target);
|
||||
};
|
||||
|
||||
return exports;
|
||||
});
|
||||
-135
@@ -1,135 +0,0 @@
|
||||
/**
|
||||
* This module provides methods for attaching a terminal to a terminado WebSocket stream.
|
||||
*
|
||||
* @module xterm/addons/terminado/terminado
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
(function (attach) {
|
||||
if (typeof exports === 'object' && typeof module === 'object') {
|
||||
/*
|
||||
* CommonJS environment
|
||||
*/
|
||||
module.exports = attach(require('../../xterm'));
|
||||
} else if (typeof define == 'function') {
|
||||
/*
|
||||
* Require.js is available
|
||||
*/
|
||||
define(['../../xterm'], attach);
|
||||
} else {
|
||||
/*
|
||||
* Plain browser environment
|
||||
*/
|
||||
attach(window.Terminal);
|
||||
}
|
||||
})(function (Xterm) {
|
||||
'use strict';
|
||||
|
||||
var exports = {};
|
||||
|
||||
/**
|
||||
* Attaches the given terminal to the given socket.
|
||||
*
|
||||
* @param {Xterm} term - The terminal to be attached to the given socket.
|
||||
* @param {WebSocket} socket - The socket to attach the current terminal.
|
||||
* @param {boolean} bidirectional - Whether the terminal should send data
|
||||
* to the socket as well.
|
||||
* @param {boolean} buffered - Whether the rendering of incoming data
|
||||
* should happen instantly or at a maximum
|
||||
* frequency of 1 rendering per 10ms.
|
||||
*/
|
||||
exports.terminadoAttach = function (term, socket, bidirectional, buffered) {
|
||||
bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
|
||||
term.socket = socket;
|
||||
|
||||
term._flushBuffer = function () {
|
||||
term.write(term._attachSocketBuffer);
|
||||
term._attachSocketBuffer = null;
|
||||
clearTimeout(term._attachSocketBufferTimer);
|
||||
term._attachSocketBufferTimer = null;
|
||||
};
|
||||
|
||||
term._pushToBuffer = function (data) {
|
||||
if (term._attachSocketBuffer) {
|
||||
term._attachSocketBuffer += data;
|
||||
} else {
|
||||
term._attachSocketBuffer = data;
|
||||
setTimeout(term._flushBuffer, 10);
|
||||
}
|
||||
};
|
||||
|
||||
term._getMessage = function (ev) {
|
||||
var data = JSON.parse(ev.data)
|
||||
if( data[0] == "stdout" ) {
|
||||
if (buffered) {
|
||||
term._pushToBuffer(data[1]);
|
||||
} else {
|
||||
term.write(data[1]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
term._sendData = function (data) {
|
||||
socket.send(JSON.stringify(['stdin', data]));
|
||||
};
|
||||
|
||||
term._setSize = function (size) {
|
||||
socket.send(JSON.stringify(['set_size', size.rows, size.cols]));
|
||||
};
|
||||
|
||||
socket.addEventListener('message', term._getMessage);
|
||||
|
||||
if (bidirectional) {
|
||||
term.on('data', term._sendData);
|
||||
}
|
||||
term.on('resize', term._setSize);
|
||||
|
||||
socket.addEventListener('close', term.terminadoDetach.bind(term, socket));
|
||||
socket.addEventListener('error', term.terminadoDetach.bind(term, socket));
|
||||
};
|
||||
|
||||
/**
|
||||
* Detaches the given terminal from the given socket
|
||||
*
|
||||
* @param {Xterm} term - The terminal to be detached from the given socket.
|
||||
* @param {WebSocket} socket - The socket from which to detach the current
|
||||
* terminal.
|
||||
*/
|
||||
exports.terminadoDetach = function (term, socket) {
|
||||
term.off('data', term._sendData);
|
||||
|
||||
socket = (typeof socket == 'undefined') ? term.socket : socket;
|
||||
|
||||
if (socket) {
|
||||
socket.removeEventListener('message', term._getMessage);
|
||||
}
|
||||
|
||||
delete term.socket;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches the current terminal to the given socket
|
||||
*
|
||||
* @param {WebSocket} socket - The socket to attach the current terminal.
|
||||
* @param {boolean} bidirectional - Whether the terminal should send data
|
||||
* to the socket as well.
|
||||
* @param {boolean} buffered - Whether the rendering of incoming data
|
||||
* should happen instantly or at a maximum
|
||||
* frequency of 1 rendering per 10ms.
|
||||
*/
|
||||
Xterm.prototype.terminadoAttach = function (socket, bidirectional, buffered) {
|
||||
return exports.terminadoAttach(this, socket, bidirectional, buffered);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detaches the current terminal from the given socket.
|
||||
*
|
||||
* @param {WebSocket} socket - The socket from which to detach the current
|
||||
* terminal.
|
||||
*/
|
||||
Xterm.prototype.terminadoDetach = function (socket) {
|
||||
return exports.terminadoDetach(this, socket);
|
||||
};
|
||||
|
||||
return exports;
|
||||
});
|
||||
Vendored
-2261
File diff suppressed because it is too large
Load Diff
Vendored
-5132
File diff suppressed because it is too large
Load Diff
Vendored
-1
File diff suppressed because one or more lines are too long
+2
-4
@@ -23,7 +23,6 @@
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-family: "Roboto","Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.846;
|
||||
}
|
||||
|
||||
@@ -71,9 +70,8 @@
|
||||
<body>
|
||||
|
||||
<div class="content">
|
||||
<h1>🙁</h1>
|
||||
<h2>Something has gone wrong</h2>
|
||||
<p>This app is currently not responding. Try refreshing the page.</p>
|
||||
<h1>⌛</h1>
|
||||
<p>This app is currently not responding. Please try refreshing the page in a few minutes.</p>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
<!DOCTYPE html>
|
||||
<html ng-app="Application" ng-controller="FileManagerController">
|
||||
<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> FileManager </title>
|
||||
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
<link rel="apple-touch-icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar">
|
||||
|
||||
<!-- CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.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 %>"/>
|
||||
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Angularjs scripts -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-animate.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-base64.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-sanitize.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- colors -->
|
||||
<script type="text/javascript" src="/3rdparty/js/colors.js?<%= revision %>"></script>
|
||||
|
||||
<!-- moment -->
|
||||
<script type="text/javascript" src="/3rdparty/js/moment.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- https://github.com/data-uri/mimer -->
|
||||
<script type="text/javascript" src="/3rdparty/js/mimer.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- https://github.com/Templarian/ui.bootstrap.contextMenu -->
|
||||
<script type="text/javascript" src="/3rdparty/js/contextMenu.js?<%= revision %>"></script>
|
||||
|
||||
<!-- WARNING this adds an AMD loader! Make sure script tag includes like mimer are above -->
|
||||
<!-- monaco-editor -->
|
||||
<script type="text/javascript" src="/3rdparty/vs/loader.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Main Application -->
|
||||
<script type="text/javascript" src="/js/filemanager.js?<%= revision %>"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="filemanager" ng-drop="drop($event)" ng-dragover="dragEnter($event)" ng-dragleave="dragExit($event)">
|
||||
|
||||
<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> {{ 'main.offline' | tr }}</a>
|
||||
|
||||
<div class="restart-banner animateMe" ng-show="restartBusy" ng-cloak><i class="fa fa-circle-notch fa-spin"></i> {{ 'filemanager.status.restartingApp' | tr}}</div>
|
||||
|
||||
<!-- Modal image/video viewer -->
|
||||
<div class="modal fade" id="mediaViewerModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" style="max-width: 1280px; max-height: calc(100% - 60px);">
|
||||
<div class="modal-content" style="height: 100%; height: 100%; display: flex; background-color: #000; background-clip: border-box;">
|
||||
<img ng-show="mediaViewer.type === 'image'" ng-src="{{ mediaViewer.src }}" style="display: block; margin: auto; max-width: 100%; max-height: 100%;" />
|
||||
<video ng-show="mediaViewer.type === 'video'" controls preload="auto" autoplay ng-src="{{ mediaViewer.src | trustUrl}}" style="display: block; margin: auto; max-width: 100%; max-height: 100%;"></video>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal remove entry -->
|
||||
<div class="modal fade" id="entryRemoveModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<p class="text-bold text-danger" ng-show="entryRemove.error">{{ entryRemove.error }}</p>
|
||||
<!-- TODO remove fileName later once all translations have been updated -->
|
||||
<h4 ng-hide="entryRemove.error">{{ 'filemanager.removeDialog.reallyDelete' | tr:{ fileName: selected[0].fileName } }}</h4>
|
||||
<ul>
|
||||
<li ng-repeat="entry in selected">{{ entry.fileName }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.no' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="entryRemove.submit()" ng-hide="entryRemove.error" ng-disabled="entryRemove.busy"><i class="fa fa-circle-notch fa-spin" ng-show="entryRemove.busy"></i> {{ 'main.dialog.yes' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal new directory -->
|
||||
<div class="modal fade" id="newDirectoryModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'filemanager.newDirectoryDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form role="form" name="newDirectoryForm" ng-submit="newDirectory.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': newDirectory.error || (newDirectoryForm.directoryName.$dirty && newDirectoryForm.directoryName.$invalid) }">
|
||||
<input type="text" class="form-control" id="inputDirectoryName" name="directoryName" ng-model="newDirectory.name" required autofocus>
|
||||
<div class="control-label" ng-show="newDirectory.error === 'exists'">{{ 'filemanager.newDirectory.errorAlreadyExists' | tr }}</div>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="newDirectoryForm.$invalid || newDirectory.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="newDirectory.submit()" ng-disabled="newDirectory.busy"><i class="fa fa-circle-notch fa-spin" ng-show="newDirectory.busy"></i> {{ 'filemanager.newDirectoryDialog.create' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal new file -->
|
||||
<div class="modal fade" id="newFileModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'filemanager.newFileDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form role="form" name="newFileForm" ng-submit="newFile.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': newFile.error || (newFileForm.fileName.$dirty && newFileForm.fileName.$invalid) }">
|
||||
<input type="text" class="form-control" id="inputFileName" name="fileName" ng-model="newFile.name" required autofocus>
|
||||
<div class="control-label" ng-show="newFile.error === 'exists'">{{ 'filemanager.newFile.errorAlreadyExists' | tr }}</div>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="newFileForm.$invalid || newFile.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="newFile.submit()" ng-disabled="newFile.busy"><i class="fa fa-circle-notch fa-spin" ng-show="newFile.busy"></i> {{ 'filemanager.newFileDialog.create' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal rename entry -->
|
||||
<div class="modal fade" id="renameEntryModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'filemanager.renameDialog.title' | tr:{ fileName: renameEntry.entry.fileName } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form role="form" name="renameEntryForm" ng-submit="renameEntry.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (renameEntryForm.newName.$dirty && renameEntryForm.newName.$invalid) }">
|
||||
<label class="control-label">{{ 'filemanager.renameDialog.newName' | tr }}</label>
|
||||
<div class="control-label" ng-show="renameEntry.error">{{ renameEntry.error }}</div>
|
||||
<input type="text" class="form-control" id="inputNewName" name="newName" ng-model="renameEntry.newName" required autofocus>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="renameEntryForm.$invalid || renameEntry.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="renameEntry.submit()" ng-hide="renameEntry.error" ng-disabled="renameEntry.busy"><i class="fa fa-circle-notch fa-spin" ng-show="renameEntry.busy"></i> {{ 'filemanager.renameDialog.rename' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal chown entry -->
|
||||
<div class="modal fade" id="chownEntryModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<!-- TODO remove fileName later once all translations have been updated -->
|
||||
<h4 class="modal-title">{{ 'filemanager.chownDialog.title' | tr:{ fileName: selected[0].fileName } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form role="form" name="chownEntryForm" ng-submit="chownEntry.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (chownEntryForm.newOwner.$dirty && chownEntry.error) }">
|
||||
<label class="control-label">{{ 'filemanager.chownDialog.newOwner' | tr }}</label>
|
||||
<div class="control-label" for="inputNewOwner" ng-show="chownEntry.error">{{ chownEntry.error }}</div>
|
||||
<select class="form-control" id="inputNewOwner" name="newOwner" ng-model="chownEntry.newOwner" ng-options="a.value as a.name for a in owners" ng-disabled="chownEntry.busy"></select>
|
||||
</div>
|
||||
<div class="form-group" ng-show="chownEntry.showRecursiveOption">
|
||||
<input type="checkbox" id="inputNewOwnerRecursive" ng-model="chownEntry.recursive">
|
||||
<label class="control-label" for="inputNewOwnerRecursive">{{ 'filemanager.chownDialog.recursiveCheckbox' | tr }}</label>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="chownEntryForm.$invalid || chownEntry.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="chownEntry.submit()" ng-hide="chownEntry.error" ng-disabled="chownEntry.busy"><i class="fa fa-circle-notch fa-spin" ng-show="chownEntry.busy"></i> {{ 'filemanager.chownDialog.change' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal upload -->
|
||||
<div class="modal fade" id="uploadModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'filemanager.uploadingDialog.title' | tr:{ countDone: uploadStatus.countDone, count: uploadStatus.count } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-show="uploadStatus.error">
|
||||
<p class="text-danger" ng-show="uploadStatus.error === 'exists'">{{ 'filemanager.uploadingDialog.errorAlreadyExists' | tr }}</p>
|
||||
<p class="text-danger" ng-show="uploadStatus.error === 'generic'">{{ 'filemanager.uploadingDialog.errorFailed' | tr }}</p>
|
||||
</div>
|
||||
<span><b>{{ uploadStatus.sizeDone | prettyByteSize }}</b> (total {{ uploadStatus.size | prettyByteSize }})</span>
|
||||
<div class="progress progress-striped active" ng-hide="uploadStatus.error">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ uploadStatus.percentDone || 0 }}%"></div>
|
||||
</div>
|
||||
<p class="no-wrap" ng-hide="uploadStatus.error">{{ uploadStatus.fileName }}</p>
|
||||
</div>
|
||||
<div class="modal-footer" style="text-align: left;">
|
||||
<small ng-hide="uploadStatus.error">{{ 'filemanager.uploadingDialog.closeWarning' | tr }}</small>
|
||||
<button class="btn btn-default pull-right" ng-show="uploadStatus.error" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button class="btn btn-primary pull-right" ng-show="uploadStatus.error === 'generic'" ng-click="retryUpload(false)">{{ 'filemanager.uploadingDialog.retry' | tr }}</button>
|
||||
<button class="btn btn-danger pull-right" ng-show="uploadStatus.error === 'exists'" ng-click="retryUpload(true)">{{ 'filemanager.uploadingDialog.overwrite' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal extract -->
|
||||
<div class="modal fade" id="extractModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'filemanager.extractDialog.title' | tr:{ fileName: extractStatus.fileName } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-show="extractStatus.error">
|
||||
<p class="text-danger">{{ extractStatus.error }}</p>
|
||||
</div>
|
||||
<div class="progress progress-striped active" ng-hide="extractStatus.error">
|
||||
<div class="progress-bar" role="progressbar" style="width: 100%">
|
||||
</div>
|
||||
</div>
|
||||
<p class="no-wrap" ng-hide="extractStatus.error">{{ extractStatus.fileName }}</p>
|
||||
</div>
|
||||
<div class="modal-footer" style="text-align: left;">
|
||||
<small ng-hide="extractStatus.error">{{ 'filemanager.extractDialog.closeWarning' | tr }}</small>
|
||||
<button class="btn btn-primary pull-right" ng-show="extractStatus.error" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal editor close -->
|
||||
<div class="modal fade" id="textEditorCloseModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'filemanager.textEditorCloseDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-bold text-danger">{{ 'filemanager.textEditorCloseDialog.details' | tr }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" ng-click="textEditor.onClose()">{{ 'filemanager.textEditorCloseDialog.dontSave' | tr }}</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="textEditor.saveAndClose()"><i class="fa fa-circle-notch fa-spin" ng-show="textEditor.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-container animateMe ng-hide layout-root" ng-show="initialized">
|
||||
<div ng-show="view === 'fileTree'" class="layout-content container">
|
||||
<div class="row" ng-hide="title">
|
||||
<div class="col-md-12 text-center">
|
||||
<h3>{{ 'filemanager.notFound' | tr }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" ng-show="title">
|
||||
|
||||
<input type="file" id="uploadFileInput" style="display: none" multiple/>
|
||||
<input type="file" id="uploadFolderInput" style="display: none" multiple webkitdirectory directory/>
|
||||
|
||||
<h4 class="text-center" ng-show="type === 'app'"><a ng-href="{{ applicationLink }}" target="_blank">{{ title }}</a></h4>
|
||||
<h4 class="text-center" ng-hide="type === 'app'">{{ title }}</h4>
|
||||
|
||||
<div class="toolbar">
|
||||
<div class="btn-group" role="group" style="display: block;">
|
||||
<!-- TODO figure out why a line break in code between the two buttons results in a gap visually without any margin/padding set -->
|
||||
<button class="btn btn-primary" ng-click="goDirectoryUp()" ng-disabled="busy || cwd === '/'"><i class="fas fa-arrow-left"></i></button><button class="btn btn-primary" ng-disabled="busy" ng-click="refresh()"><i class="fas fa-sync-alt"></i></button>
|
||||
</div>
|
||||
<div class="btn-group path-parts" role="group">
|
||||
<button class="btn btn-default" ng-disabled="busy || cwd === '/'" ng-click="changeDirectory('/')" ng-drop="drop($event, '/')" ng-dragleave="dragExit($event, '/')" ng-dragover="dragEnter($event, '/')"><i class="fas fa-home"></i> {{ rootDirLabel }} </button><button class="btn btn-default" ng-disabled="busy || part.path === cwd" ng-click="changeDirectory(part.path)" ng-drop="drop($event, part.path)" ng-dragleave="dragExit($event, part.path)" ng-dragover="dragEnter($event, part.path)" ng-repeat="part in cwdParts">{{ part.name }}</button>
|
||||
</div>
|
||||
<div style="display: block;">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fas fa-plus"></i> {{ 'filemanager.toolbar.new' | tr }}</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="hand" ng-click="newFile.show()">{{ 'filemanager.toolbar.newFile' | tr }}</a></li>
|
||||
<li><a class="hand" ng-click="newDirectory.show()">{{ 'filemanager.toolbar.newFolder' | tr }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fas fa-upload"></i> {{ 'filemanager.toolbar.upload' | tr }}</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a class="hand" ng-click="onUploadFile()">{{ 'filemanager.toolbar.uploadFile' | tr }}</a></li>
|
||||
<li><a class="hand" ng-click="onUploadFolder()">{{ 'filemanager.toolbar.uploadFolder' | tr }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="btn-group" ng-show="type === 'app' || type === 'mail'">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fas fa-ellipsis-h"></i></button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a class="hand" ng-show="type === 'app'" ng-click="onRestartApp()"><i class="fas fa-sync-alt fa-fw"></i> {{ 'filemanager.toolbar.restartApp' | tr }}</a></li>
|
||||
<li><a class="hand" ng-show="type === 'mail'" ng-click="onRestartMail()"><i class="fas fa-sync-alt fa-fw"></i> {{ 'filemanager.toolbar.restartApp' | tr }}</a></li>
|
||||
<li><a class="hand" ng-href="/logs.html?{{ type === 'app' ? 'appId=' + id : 'id=mail' }}" target="_blank"><i class="fas fa-align-left fa-fw"></i> {{ 'filemanager.toolbar.openLogs' | tr }}</a></li>
|
||||
<li><a class="hand" ng-show="type === 'app'" ng-href="{{ '/terminal.html?id=' + id }}" target="_blank"><i class="fa fa-terminal fa-fw"></i> {{ 'filemanager.toolbar.openTerminal' | tr }}</a></li>
|
||||
<li role="separator" class="divider" ng-show="volumes.length"></li>
|
||||
<li class="disabled" ng-show="volumes.length"><a href="#">Volumes</a></li>
|
||||
<li ng-repeat="volume in volumes"><a class="hand" ng-href="{{ '/filemanager.html?type=volume&id=' + volume.id }}" target="_blank"><i class="fas fa-folder fa-fw"></i> {{ volume.name }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="file-list-header">
|
||||
<table class="table" style="margin: 0;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 40px;"> </th>
|
||||
<th style="">{{ 'filemanager.list.name' | tr }}</th>
|
||||
<th style="width:100px">{{ 'filemanager.list.owner' | tr }}</th>
|
||||
<th style="width: 80px">{{ 'filemanager.list.size' | tr }}</th>
|
||||
<th style="width:100px">{{ 'filemanager.list.mtime' | tr }}</th>
|
||||
<th style="width: 45px;"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="file-list" ng-class="{ 'entry-hovered': dropToBody, 'busy': busy }" context-menu="menuOptionsBlank" ng-mousedown="select($event, null)">
|
||||
<table class="table table-hover" style="margin: 0;">
|
||||
<tbody>
|
||||
<tr ng-show="busy">
|
||||
<td colspan="6"><center><h2><i class="fa fa-circle-notch fa-spin"></i></h2></center></td>
|
||||
</tr>
|
||||
<tr ng-show="!busy && entries.length === 0">
|
||||
<td colspan="" class="text-center">{{ 'filemanager.list.empty' | tr }}</td>
|
||||
</tr>
|
||||
<tr ng-hide="busy" ng-repeat="entry in entries | orderBy:sortProperty:sortAsc | orderBy:'isDirectory':true" draggable="true" ng-dragstart="dragStart($event, entry)" ng-drop="drop($event, entry)" context-menu="menuOptions" model="entry" ng-dragleave="dragExit($event, entry)" ng-dragover="dragEnter($event, entry)" ng-class="{ 'entry-hovered': entry.hovered, 'entry-selected': isSelected(entry) }">
|
||||
<td style="width: 40px" ng-mousedown="select($event, entry)" ng-dblclick="open(entry)" class="text-center">
|
||||
<i class="fas fa-lg {{ entry.icon }}" ng-class="{ 'text-primary': entry.isDirectory && !isSelected(entry) }"></i>
|
||||
</td>
|
||||
<td class="elide-table-cell" ng-mousedown="select($event, entry)" ng-dblclick="open(entry)">{{ entry.fileName }}<span ng-show="entry.isSymbolicLink" class="text-muted" style="margin-left: 20px;">{{ 'filemanager.list.symlink' | tr:{ target: entry.target } }}</span></td>
|
||||
<td style="width:100px" class="elide-table-cell" ng-mousedown="select($event, entry)" ng-dblclick="open(entry)">{{ entry.uid | prettyOwner }}</td>
|
||||
<td style="width: 80px" class="elide-table-cell" ng-mousedown="select($event, entry)" ng-dblclick="open(entry)">{{ entry.size | prettyByteSize }}</td>
|
||||
<td style="width:100px" class="elide-table-cell" ng-mousedown="select($event, entry)" ng-dblclick="open(entry)" uib-tooltip="{{ entry.mtime | prettyLongDate }}" tooltip-append-to-body="true">{{ entry.mtime | prettyDate }}</td>
|
||||
<td style="width: 45px; padding: 7px;">
|
||||
<button type="button" class="btn btn-xs btn-default context-menu-action" context-menu="menuOptions" model="entry" context-menu-on="click" ng-click="onEntryContextMenu($event, entry)"><i class="fas fa-ellipsis-h"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="view === 'textEditor'" class="text-editor">
|
||||
<div>
|
||||
<div class="toolbar">
|
||||
<div><span>{{ textEditor.entry.fileName }}</span></div>
|
||||
<button type="button" class="btn btn-primary" ng-click="textEditor.maybeClose()">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="textEditor.save()" ng-disabled="textEditor.busy"><i class="fa fa-circle-notch fa-spin" ng-show="textEditor.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="textEditorContainer" style="flex-grow: 2; border: 0px solid black"></div>
|
||||
</div>
|
||||
|
||||
<footer class="text-center ng-cloak">
|
||||
<span class="text-muted" ng-bind-html="status.footer | markdown2html"></span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,133 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="16"
|
||||
height="16"
|
||||
id="svg2"
|
||||
sodipodi:version="0.32"
|
||||
inkscape:version="0.91 r13725"
|
||||
version="1.0"
|
||||
sodipodi:docname="avatar-default-symbolic.svg"
|
||||
inkscape:output_extension="org.inkscape.output.svg.inkscape">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#e7e7e7"
|
||||
borderopacity="1"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="11.964497"
|
||||
inkscape:cx="6.5536056"
|
||||
inkscape:cy="-0.025360958"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
inkscape:showpageshadow="false"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1030"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-global="true">
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="0,112"
|
||||
id="guide2383" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="78.156291,0"
|
||||
id="guide2389" />
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid3672"
|
||||
visible="true"
|
||||
enabled="true" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="22.008699,4.1542523"
|
||||
id="guide2950" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="11.22532,22.008699"
|
||||
id="guide2952" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Calque 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccsccccc"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none"
|
||||
id="path3935"
|
||||
d="m -13.771529,5.9050966 c 0.181174,0.8569201 0.2823,1.5051186 0.135325,2.3620387 -1.145861,0.9506717 -4.076448,1.3778558 -4.072056,2.3620387 l -0.393673,2.558875 c 0,0.978388 2.731928,1.771529 6.101933,1.771529 3.370005,0 6.101933,-0.793141 6.101933,-1.771529 L -6.29174,10.629174 c -0.0047,-0.8423279 -2.952548,-1.377856 -4.084358,-2.3620387 -0.09668,-0.7953524 -0.01972,-1.5666863 0.147627,-2.3620387 l -3.543058,0 z" />
|
||||
<path
|
||||
transform="matrix(0.34209356,0,0,0.34209356,-8.638748,-12.26548)"
|
||||
d="m -9.75,73.09375 c -3.766412,0.121068 -7.468069,1.386362 -11.40625,3.25 a 1.25331,1.25331 0 0 0 -0.6875,1.4375 l 0.625,2.53125 a 1.25331,1.25331 0 0 0 0.78125,0.84375 c 0.161757,0.06256 0.275429,0.183794 0.71875,0.3125 2.335298,0.677989 5.907957,1.15625 9.90625,1.15625 3.9982931,0 7.5709518,-0.478261 9.90625,-1.15625 0.44332111,-0.128707 0.55699247,-0.24994 0.71875,-0.3125 a 1.25331,1.25331 0 0 0 0.78125,-0.8125 L 2.25,78.03125 a 1.25331,1.25331 0 0 0 -0.53125,-1.375 C -2.2051532,74.042333 -5.9835879,72.972682 -9.75,73.09375 z"
|
||||
id="path3937"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none"
|
||||
inkscape:original="M -9.71875 74.34375 C -13.230599 74.456635 -16.76467 75.641953 -20.625 77.46875 L -20 80 C -19.731211 80.103955 -19.729288 80.147142 -19.375 80.25 C -17.218663 80.876033 -13.703662 81.375 -9.8125 81.375 C -5.9213382 81.375 -2.4063369 80.876033 -0.25 80.25 C 0.10428761 80.147142 0.10621054 80.103955 0.375 80 L 1.03125 77.6875 C -2.7172738 75.190412 -6.2069011 74.230865 -9.71875 74.34375 z "
|
||||
inkscape:radius="1.2531847"
|
||||
sodipodi:type="inkscape:offset" />
|
||||
<rect
|
||||
transform="matrix(0.9205234,-0.39068744,0.39068744,0.9205234,0,0)"
|
||||
ry="1.1810193"
|
||||
rx="1.1810193"
|
||||
y="-2.754653"
|
||||
x="-15.569602"
|
||||
height="2.1871843"
|
||||
width="1.0935922"
|
||||
id="rect3939"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none"
|
||||
id="rect3941"
|
||||
width="1.0935922"
|
||||
height="2.1871843"
|
||||
x="6.5567312"
|
||||
y="6.6361833"
|
||||
rx="1.1810193"
|
||||
ry="1.1810193"
|
||||
transform="matrix(-0.9205234,-0.39068744,-0.39068744,0.9205234,0,0)" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccccc"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none"
|
||||
id="path3943"
|
||||
d="m -12,0 c -1.630647,0 -2.952548,1.2337743 -2.952548,2.7557118 0.01278,0.5632387 0.06085,1.232346 0.393673,2.7557117 0.196837,0.5905097 1.558851,2.1652021 1.574692,2.3620387 0.381733,0.1968365 1.771529,0.1968365 2.165203,0 0,-0.1968366 1.181019,-1.771529 1.377855,-2.3620387 C -9.066594,3.9281919 -9.06754,3.3462214 -9.047452,2.7557118 -9.047452,1.2337743 -10.369352,0 -12,0 z" />
|
||||
<path
|
||||
id="path3157"
|
||||
d="m 38,0 c -1.630647,0 -2.9375,1.2280625 -2.9375,2.75 0.0037,0.1620664 0.01579,0.3963239 0.03125,0.59375 -0.27885,0.118349 -0.299198,0.6610508 -0.0625,1.21875 0.09386,0.2211566 0.213411,0.3909677 0.34375,0.53125 0.03167,0.1567366 0.02336,0.2271022 0.0625,0.40625 0.196837,0.5905097 1.577909,2.1781634 1.59375,2.375 0.381733,0.1968365 1.762576,0.1968365 2.15625,0 0,-0.1968366 1.178164,-1.7844903 1.375,-2.375 C 40.60622,5.3151913 40.62213,5.1903792 40.65625,5.03125 40.764832,4.8997227 40.857512,4.7509639 40.9375,4.5625 41.162363,4.0326858 41.147829,3.5269131 40.90625,3.375 40.920493,3.1615298 40.931227,2.9343906 40.9375,2.75 40.9375,1.2280625 39.630648,0 38,0 z m -1.78125,8.40625 c -1.233461,0.8706787 -3.941711,1.2750309 -3.9375,2.21875 l -0.375,2.5625 c 0,0.519013 0.775005,0.988493 2,1.3125 l 0.1875,0.71875 A 0.42874928,0.42874928 0 0 0 34.375,15.5 c 0.05534,0.0214 0.09834,0.04972 0.25,0.09375 C 35.42389,15.825686 36.63221,16 38,16 39.36779,16 40.60736,15.825686 41.40625,15.59375 41.557907,15.54972 41.569664,15.5214 41.625,15.5 a 0.42874928,0.42874928 0 0 0 0.28125,-0.28125 L 42.125,14.5 c 1.208619,-0.323691 1.96875,-0.797472 1.96875,-1.3125 l -0.375,-2.5625 C 43.714419,9.848863 41.21753,9.3437322 39.9375,8.5 A 0.97584188,0.97584188 0 0 1 39.625,8.75 C 39.020006,9.0524961 38.608286,9 38.09375,9 37.836482,9 37.587947,9.0004922 37.34375,8.96875 37.099553,8.937008 36.902156,8.909026 36.59375,8.75 a 0.97584188,0.97584188 0 0 1 -0.375,-0.34375 z"
|
||||
style="fill:#bebebe;fill-opacity:1;stroke:none"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:#bebebe;fill-opacity:1;stroke:none"
|
||||
d="m 8,0.7783785 c -2.256463,0 -4.0648649,1.699373 -4.0648649,3.8054054 0.00509,0.2242649 0.021845,0.5484266 0.043244,0.8216216 -0.3858679,0.1637694 -0.4140257,0.9147514 -0.086487,1.6864865 0.1298861,0.3060329 0.295314,0.5410147 0.4756757,0.7351351 0.043823,0.2168896 0.032325,0.3142604 0.086486,0.5621622 0.1511376,0.4534118 0.7470076,1.3420395 1.2972972,2.0756757 0.05396,0.563421 0.109936,1.132004 0,1.772973 -1.5856236,1.315524 -5.67094212,1.881347 -5.66486451,3.243243 L 0,16 16,16 15.91351,15.481081 c -0.0065,-1.1656 -4.098682,-1.881347 -5.664862,-3.243243 -0.06337,-0.521335 -0.07545,-1.043272 -0.04324,-1.556757 0.501434,-0.7738141 1.172201,-1.7868737 1.34054,-2.2918917 0.0605,-0.2557354 0.08252,-0.4284482 0.129729,-0.6486487 0.150255,-0.1820053 0.278504,-0.3878554 0.38919,-0.6486486 0.311162,-0.7331483 0.291049,-1.4330284 -0.04324,-1.6432432 0.01971,-0.2953966 0.03456,-0.6097082 0.04324,-0.8648649 0,-2.1060324 -1.8084,-3.8054054 -4.064865,-3.8054054 z"
|
||||
id="path3159"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.8 KiB |
+87
-44
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<!-- this gets changed once we get the config (because angular has not loaded yet, we see template string for a flash) -->
|
||||
<title> Cloudron </title>
|
||||
<title>‎</title>
|
||||
|
||||
<link id="favicon" type="image/png" rel="icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="apple-touch-icon" href="/api/v1/cloudron/avatar">
|
||||
@@ -17,12 +17,15 @@
|
||||
<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 %>">
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.min.css?<%= revision %>">
|
||||
<!-- 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?<%= revision %>"></script>
|
||||
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- toBlob() polyfill-->
|
||||
<script type="text/javascript" src="/3rdparty/js/canvas-to-blob.min.js?<%= revision %>"></script>
|
||||
|
||||
@@ -36,6 +39,7 @@
|
||||
<script type="text/javascript" src="/3rdparty/js/angular.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-route.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-animate.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-base64.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js?<%= revision %>"></script>
|
||||
@@ -52,13 +56,21 @@
|
||||
<!-- 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>
|
||||
|
||||
<script type="text/javascript" src="/3rdparty/js/Chart.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>
|
||||
|
||||
<!-- Chart.js https://www.chartjs.org/ -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/Chart/Chart.min.css?<%= revision %>"/>
|
||||
<script type="text/javascript" src="/3rdparty/Chart/Chart.min.js?<%= revision %>"></script>
|
||||
|
||||
<script type="text/javascript" src="/3rdparty/js/ansi_up.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/clipboard.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-1.6.4.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-target-blank.min.js?<%= revision %>"></script>
|
||||
<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>
|
||||
@@ -93,7 +105,26 @@
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<a class="offline-banner animateMe" ng-show="client.offline" ng-cloak href="https://cloudron.io/documentation/troubleshooting/" target="_blank"><i class="fa fa-circle-notch fa-spin"></i> Cloudron is offline. Reconnecting...</a>
|
||||
<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> {{ 'main.offline' | tr }}</a>
|
||||
|
||||
<!-- Modal reboot server -->
|
||||
<div class="modal fade" id="rebootModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'main.rebootDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-bold">{{ 'main.rebootDialog.warning' | tr }}</p>
|
||||
<p>{{ 'main.rebootDialog.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="reboot.submit()" ng-disabled="reboot.busy"><i class="fa fa-circle-notch fa-spin" ng-show="reboot.busy"></i> {{ 'main.rebootDialog.rebootAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="animateMe ng-hide layout-root" ng-show="initialized">
|
||||
|
||||
@@ -114,42 +145,55 @@
|
||||
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="nav navbar-nav navbar-right" ng-hide="hideNavBarActions">
|
||||
<li ng-show="user.isAtLeastOwner && (subscription.plan.id === 'free' || subscription.plan.id === 'expired')">
|
||||
<a ng-click="openSubscriptionSetup()" style="cursor: pointer">
|
||||
<span class="badge badge-success">{{ subscription.plan.id === 'free' ? 'Setup' : 'Reactivate' }} Subscription</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a ng-class="{ active: isActive('/apps')}" href="#/apps"><i class="fa fa-cloud-download-alt fa-fw"></i> My Apps</a>
|
||||
</li>
|
||||
<li ng-show="user.isAtLeastAdmin">
|
||||
<a ng-class="{ active: isActive('/appstore')}" href="#/appstore"><i class="fa fa-th fa-fw"></i> App Store</a>
|
||||
</li>
|
||||
<li ng-show="user.isAtLeastUserManager">
|
||||
<a ng-class="{ active: isActive('/users')}" href="#/users"><i class="fa fa-users fa-fw"></i> Users</a>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
<a href="" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><img ng-src="{{user.avatarUrl}}" style="width: 24px; height: 24px;"/> {{user.username}} <span class="badge badge-danger" ng-show="notifications.length">{{ notifications.length }}</span> <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a href="#/profile"><i class="fa fa-user fa-fw"></i> Profile</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin" class="divider"></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/backups"><i class="fa fa-archive fa-fw"></i> Backups</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/domains"><i class="fa fa-globe fa-fw"></i> Domains & Certs</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/email"><i class="fa fa-envelope fa-fw"></i> Email</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/activity"><i class="fa fa-list-alt fa-fw"></i> Event Log</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/graphs"><i class="fa fa-chart-bar fa-fw"></i> Graphs</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/network"><i class="fas fa-network-wired fa-fw"></i> Network</a></li>
|
||||
<li><a href="#/notifications"><i class="fa fa-bell fa-fw"></i> Notifications <span class="badge badge-danger" ng-show="notifications.length">{{ notifications.length }}</span></a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> Settings</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin" class="divider"></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/support"><i class="fa fa-comment fa-fw"></i> Support</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/system"><i class="fa fa-cogs fa-fw"></i> System Info</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="" ng-click="logout($event)"><i class="fa fa-sign-out-alt fa-fw"></i> Logout</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<li ng-show="user.isAtLeastOwner && (subscription.plan.id === 'free' || subscription.plan.id === 'expired')">
|
||||
<a ng-click="openSubscriptionSetup()" style="cursor: pointer">
|
||||
<span class="badge badge-success" ng-show="subscription.plan.id === 'free'">Set up Subscription</span>
|
||||
<span class="badge badge-danger" ng-show="subscription.plan.id !== 'free'">Reactivate Subscription</span>
|
||||
</a>
|
||||
</li>
|
||||
<li ng-show="!user.isAtLeastOwner && subscription.plan.id === 'expired'">
|
||||
<a>
|
||||
<span class="badge badge-danger">Subscription Expired</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a ng-class="{ active: isActive('/apps')}" href="#/apps"><i class="fa fa-th fa-fw"></i> {{ 'apps.title' | tr }}</a>
|
||||
</li>
|
||||
<li ng-show="user.isAtLeastAdmin">
|
||||
<a ng-class="{ active: isActive('/appstore')}" href="#/appstore"><i class="fa fa-cloud-download-alt fa-fw"></i> {{ 'appstore.title' | tr }}</a>
|
||||
</li>
|
||||
<li ng-show="user.isAtLeastUserManager">
|
||||
<a ng-class="{ active: isActive('/users')}" href="#/users"><i class="fa fa-users fa-fw"></i> {{ 'main.navbar.users' | tr }}</a>
|
||||
</li>
|
||||
<li ng-show="user.isAtLeastAdmin">
|
||||
<a href="#/notifications">
|
||||
<i class="fas fa-bell" ng-show="notificationCount"></i>
|
||||
<i class="far fa-bell" ng-hide="notificationCount"></i>
|
||||
<span class="badge badge-danger" ng-show="notificationCount">{{ notificationCount }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
<a href="" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><img ng-src="{{user.avatarUrl}}" style="width: 24px; height: 24px;"/> {{user.username}} <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<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="#/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>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/network"><i class="fas fa-network-wired fa-fw"></i> {{ 'network.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/services"><i class="fa fa-cogs fa-fw"></i> {{ 'services.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> {{ 'settings.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/volumes"><i class="fa fa-hdd fa-fw"></i> {{ 'volumes.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin" class="divider"></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/support"><i class="fa fa-comment fa-fw"></i> {{ 'support.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/system"><i class="fa fa-chart-area fa-fw"></i> {{ 'system.title' | tr }}</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="" ng-click="logout($event)"><i class="fa fa-sign-out-alt fa-fw"></i> {{ 'main.logout' | tr }}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -158,7 +202,6 @@
|
||||
|
||||
<footer class="text-center ng-cloak">
|
||||
<span class="text-muted" ng-bind-html="config.footer | markdown2html"></span>
|
||||
<span class="version">v{{config.version}}</span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
+1550
-257
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+79
-173
@@ -1,7 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular:false */
|
||||
/* global showdown:false */
|
||||
/* global moment:false */
|
||||
/* global $:false */
|
||||
/* global ERROR,ISTATES,HSTATES,RSTATES */
|
||||
@@ -19,67 +18,8 @@ if (search.accessToken) {
|
||||
window.location.search = encodeURIComponent(Object.keys(search).map(function (key) { return key + '=' + search[key]; }).join('&'));
|
||||
}
|
||||
|
||||
// poor man's async in the global namespace
|
||||
function asyncForEachParallel(items, handler, callback) {
|
||||
var alreadyDone = 0;
|
||||
var errored = false;
|
||||
|
||||
if (items.length === 0) return callback();
|
||||
|
||||
function done(error) {
|
||||
// do nothing if already called back due to error
|
||||
if (errored) return;
|
||||
|
||||
if (error) {
|
||||
errored = true;
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
++alreadyDone;
|
||||
|
||||
// we are done
|
||||
if (alreadyDone === items.length) callback();
|
||||
}
|
||||
|
||||
for (var i = 0; i < items.length; ++i) {
|
||||
handler(items[i], done);
|
||||
}
|
||||
}
|
||||
|
||||
function asyncForEach(items, handler, callback) {
|
||||
var cur = 0;
|
||||
|
||||
if (items.length === 0) return callback();
|
||||
|
||||
(function iterator() {
|
||||
handler(items[cur], function (error) {
|
||||
if (error) return callback(error);
|
||||
if (cur >= items.length-1) return callback();
|
||||
++cur;
|
||||
|
||||
iterator();
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
function asyncSeries(funcs, callback) {
|
||||
var cur = 0;
|
||||
|
||||
if (funcs.length === 0) return callback();
|
||||
|
||||
(function iterator() {
|
||||
funcs[cur](function (error) {
|
||||
if (error) return callback(error);
|
||||
if (cur >= funcs.length-1) return callback();
|
||||
++cur;
|
||||
|
||||
iterator();
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['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.bootstrap-slider', 'ngTld', 'ui.multiselect']);
|
||||
|
||||
app.config(['NotificationProvider', function (NotificationProvider) {
|
||||
NotificationProvider.setOptions({
|
||||
@@ -129,9 +69,9 @@ app.config(['$routeProvider', function ($routeProvider) {
|
||||
}).when('/backups', {
|
||||
controller: 'BackupsController',
|
||||
templateUrl: 'views/backups.html?<%= revision %>'
|
||||
}).when('/graphs', {
|
||||
controller: 'GraphsController',
|
||||
templateUrl: 'views/graphs.html?<%= revision %>'
|
||||
}).when('/branding', {
|
||||
controller: 'BrandingController',
|
||||
templateUrl: 'views/branding.html?<%= revision %>'
|
||||
}).when('/network', {
|
||||
controller: 'NetworkController',
|
||||
templateUrl: 'views/network.html?<%= revision %>'
|
||||
@@ -141,7 +81,10 @@ app.config(['$routeProvider', function ($routeProvider) {
|
||||
}).when('/email', {
|
||||
controller: 'EmailsController',
|
||||
templateUrl: 'views/emails.html?<%= revision %>'
|
||||
}).when('/email/:domain', {
|
||||
}).when('/emails-eventlog', {
|
||||
controller: 'EmailsEventlogController',
|
||||
templateUrl: 'views/emails-eventlog.html?<%= revision %>'
|
||||
}).when('/email/:domain/:view?', {
|
||||
controller: 'EmailController',
|
||||
templateUrl: 'views/email.html?<%= revision %>'
|
||||
}).when('/notifications', {
|
||||
@@ -150,15 +93,21 @@ app.config(['$routeProvider', function ($routeProvider) {
|
||||
}).when('/settings', {
|
||||
controller: 'SettingsController',
|
||||
templateUrl: 'views/settings.html?<%= revision %>'
|
||||
}).when('/activity', {
|
||||
controller: 'ActivityController',
|
||||
templateUrl: 'views/activity.html?<%= revision %>'
|
||||
}).when('/eventlog', {
|
||||
controller: 'EventLogController',
|
||||
templateUrl: 'views/eventlog.html?<%= revision %>'
|
||||
}).when('/support', {
|
||||
controller: 'SupportController',
|
||||
templateUrl: 'views/support.html?<%= revision %>'
|
||||
}).when('/system', {
|
||||
controller: 'SystemController',
|
||||
templateUrl: 'views/system.html?<%= revision %>'
|
||||
}).when('/services', {
|
||||
controller: 'ServicesController',
|
||||
templateUrl: 'views/services.html?<%= revision %>'
|
||||
}).when('/volumes', {
|
||||
controller: 'VolumesController',
|
||||
templateUrl: 'views/volumes.html?<%= revision %>'
|
||||
}).otherwise({ redirectTo: '/'});
|
||||
}]);
|
||||
|
||||
@@ -236,6 +185,35 @@ app.filter('appProgressMessage', function () {
|
||||
};
|
||||
});
|
||||
|
||||
// see apps.js $scope.states
|
||||
app.filter('selectedStateFilter', function () {
|
||||
return function selectedStateFilter(apps, selectedState) {
|
||||
return apps.filter(function (app) {
|
||||
if (!selectedState || !selectedState.state) return true;
|
||||
|
||||
if (selectedState.state === 'running') return app.runState === 'running' && app.health === 'healthy' && app.installationState === 'installed';
|
||||
if (selectedState.state === 'stopped') return app.runState === 'stopped';
|
||||
|
||||
return app.runState === 'running' && (app.health !== 'healthy' || app.installationState !== 'installed'); // not responding
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('selectedGroupAccessFilter', function () {
|
||||
return function selectedGroupAccessFilter(apps, group) {
|
||||
return apps.filter(function (app) {
|
||||
if (!group.id) return true; // case for no filter entry
|
||||
if (!app.accessRestriction) return true;
|
||||
|
||||
if (!app.accessRestriction.groups) return false;
|
||||
|
||||
if (app.accessRestriction.groups.indexOf(group.id) !== -1) return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('selectedTagFilter', function () {
|
||||
return function selectedTagFilter(apps, selectedTags) {
|
||||
return apps.filter(function (app) {
|
||||
@@ -258,7 +236,10 @@ app.filter('selectedDomainFilter', function () {
|
||||
if (selectedDomain._alldomains) return true; // magic domain for single select, see apps.js ALL_DOMAINS_DOMAIN
|
||||
|
||||
if (selectedDomain.domain === app.domain) return true;
|
||||
return !!app.alternateDomains.find(function (ad) { return ad.domain === selectedDomain.domain; });
|
||||
if (app.aliasDomains.find(function (ad) { return ad.domain === selectedDomain.domain; })) return true;
|
||||
if (app.redirectDomains.find(function (ad) { return ad.domain === selectedDomain.domain; })) return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
});
|
||||
@@ -267,7 +248,11 @@ app.filter('appSearchFilter', function () {
|
||||
return function appSearchFilter(apps, appSearch) {
|
||||
return apps.filter(function (app) {
|
||||
if (!appSearch) return true;
|
||||
return app.fqdn.indexOf(appSearch) !== -1 || (app.label && app.label.indexOf(appSearch) !== -1);
|
||||
appSearch = appSearch.toLowerCase();
|
||||
return app.fqdn.indexOf(appSearch) !== -1
|
||||
|| (app.label && app.label.toLowerCase().indexOf(appSearch) !== -1)
|
||||
|| (app.manifest.title && app.manifest.title.toLowerCase().indexOf(appSearch) !== -1)
|
||||
|| (appSearch.length >=6 && app.id.indexOf(appSearch) !== -1);
|
||||
});
|
||||
};
|
||||
});
|
||||
@@ -278,27 +263,6 @@ app.filter('prettyDomains', function () {
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('prettyMemory', function () {
|
||||
return function (memory) {
|
||||
// Adjust the default memory limit if it changes
|
||||
return memory ? Math.floor(memory / 1024 / 1024) : 256;
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('prettyMailSize', function () {
|
||||
return function (size) {
|
||||
if (!size) return '0 kB';
|
||||
var i = Math.floor(Math.log(size) / Math.log(1024));
|
||||
return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]; };
|
||||
});
|
||||
|
||||
app.filter('prettyDiskSize', function () {
|
||||
return function (size) {
|
||||
if (!size) return 'Not available yet';
|
||||
var i = Math.floor(Math.log(size) / Math.log(1024));
|
||||
return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]; };
|
||||
});
|
||||
|
||||
app.filter('installationActive', function () {
|
||||
return function (app) {
|
||||
if (app.installationState === ISTATES.ERROR) return false;
|
||||
@@ -309,7 +273,7 @@ app.filter('installationActive', function () {
|
||||
|
||||
// this appears in the app grid
|
||||
app.filter('installationStateLabel', function () {
|
||||
return function(app, user) {
|
||||
return function(app) {
|
||||
if (!app) return '';
|
||||
|
||||
var waiting = app.progress === 0 ? ' (Queued)' : '';
|
||||
@@ -330,6 +294,7 @@ app.filter('installationStateLabel', function () {
|
||||
return 'Migrating data' + waiting;
|
||||
case ISTATES.PENDING_UNINSTALL: return 'Uninstalling' + waiting;
|
||||
case ISTATES.PENDING_RESTORE: return 'Restoring' + waiting;
|
||||
case ISTATES.PENDING_IMPORT: return 'Importing' + waiting;
|
||||
case ISTATES.PENDING_UPDATE: return 'Updating' + waiting;
|
||||
case ISTATES.PENDING_BACKUP: return 'Backing up' + waiting;
|
||||
case ISTATES.PENDING_START: return 'Starting' + waiting;
|
||||
@@ -367,6 +332,7 @@ app.filter('taskName', function () {
|
||||
case ISTATES.PENDING_DATA_DIR_MIGRATION: return 'data migration';
|
||||
case ISTATES.PENDING_UNINSTALL: return 'uninstall';
|
||||
case ISTATES.PENDING_RESTORE: return 'restore';
|
||||
case ISTATES.PENDING_IMPORT: return 'import';
|
||||
case ISTATES.PENDING_UPDATE: return 'update';
|
||||
case ISTATES.PENDING_BACKUP: return 'backup';
|
||||
case ISTATES.PENDING_START: return 'start app';
|
||||
@@ -422,82 +388,15 @@ app.filter('prettyHref', function () {
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('prettyDate', function () {
|
||||
// http://ejohn.org/files/pretty.js
|
||||
return function prettyDate(utc) {
|
||||
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);
|
||||
|
||||
if (isNaN(day_diff) || day_diff < 0)
|
||||
return 'just now';
|
||||
|
||||
return day_diff === 0 && (
|
||||
diff < 60 && 'just now' ||
|
||||
diff < 120 && '1 minute ago' ||
|
||||
diff < 3600 && Math.floor( diff / 60 ) + ' minutes ago' ||
|
||||
diff < 7200 && '1 hour ago' ||
|
||||
diff < 86400 && Math.floor( diff / 3600 ) + ' hours ago') ||
|
||||
day_diff === 1 && 'Yesterday' ||
|
||||
day_diff < 7 && day_diff + ' days ago' ||
|
||||
day_diff < 31 && Math.ceil( day_diff / 7 ) + ' weeks ago' ||
|
||||
day_diff < 365 && Math.round( day_diff / 30 ) + ' months ago' ||
|
||||
Math.round( day_diff / 365 ) + ' years ago';
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('prettyLongDate', function () {
|
||||
return function prettyLongDate(utc) {
|
||||
return moment(utc).format('MMMM Do YYYY, h:mm:ss a'); // this converts utc into browser timezone and not cloudron timezone!
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('prettyShortDate', function () {
|
||||
return function prettyShortDate(utc) {
|
||||
return moment(utc).format('MMMM Do YYYY'); // this converts utc into browser timezone and not cloudron timezone!
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('prettyEmailAddresses', function () {
|
||||
return function prettyEmailAddresses(addresses) {
|
||||
if (!addresses || addresses === '<>') return '<>';
|
||||
if (!addresses) return '';
|
||||
if (addresses === '<>') return '<>';
|
||||
if (Array.isArray(addresses)) return addresses.map(function (a) { return a.slice(1, -1); }).join(', ');
|
||||
return addresses.slice(1, -1);
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('markdown2html', function () {
|
||||
var converter = new showdown.Converter({
|
||||
extensions: [ 'targetblank' ],
|
||||
simplifiedAutoLink: true,
|
||||
strikethrough: true,
|
||||
tables: true
|
||||
});
|
||||
|
||||
return function (text) {
|
||||
return converter.makeHtml(text);
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('postInstallMessage', function () {
|
||||
var SSO_MARKER = '=== sso ===';
|
||||
|
||||
return function (text, app) {
|
||||
if (!text) return '';
|
||||
if (!app) return text;
|
||||
|
||||
var parts = text.split(SSO_MARKER);
|
||||
if (parts.length === 1) {
|
||||
// [^] matches even newlines. '?' makes it non-greedy
|
||||
if (app.sso) return text.replace(/\<nosso\>[^]*?\<\/nosso\>/g, '');
|
||||
else return text.replace(/\<sso\>[^]*?\<\/sso\>/g, '');
|
||||
}
|
||||
|
||||
if (app.sso) return parts[1];
|
||||
else return parts[0];
|
||||
};
|
||||
});
|
||||
|
||||
// custom directive for dynamic names in forms
|
||||
// See http://stackoverflow.com/questions/23616578/issue-registering-form-control-with-interpolated-name#answer-23617401
|
||||
app.directive('laterName', function () { // (2)
|
||||
@@ -578,7 +477,8 @@ app.directive('tagInput', function () {
|
||||
scope: {
|
||||
inputTags: '=taglist'
|
||||
},
|
||||
link: function ($scope, element, attrs) {
|
||||
require: '^form',
|
||||
link: function ($scope, element, attrs, formCtrl) {
|
||||
$scope.defaultWidth = 200;
|
||||
$scope.tagText = ''; // current tag being edited
|
||||
$scope.placeholder = attrs.placeholder;
|
||||
@@ -586,18 +486,20 @@ app.directive('tagInput', function () {
|
||||
if ($scope.inputTags === undefined) {
|
||||
return [];
|
||||
}
|
||||
return $scope.inputTags.split(',').filter(function (tag) {
|
||||
return $scope.inputTags.split(' ').filter(function (tag) {
|
||||
return tag !== '';
|
||||
});
|
||||
};
|
||||
$scope.addTag = function () {
|
||||
var tagArray;
|
||||
if ($scope.tagText.length === 0) {
|
||||
return;
|
||||
var tagArray = $scope.tagArray();
|
||||
|
||||
// prevent adding empty or existing items
|
||||
if ($scope.tagText.length === 0 || tagArray.indexOf($scope.tagText) !== -1) {
|
||||
return $scope.tagText = '';
|
||||
}
|
||||
tagArray = $scope.tagArray();
|
||||
|
||||
tagArray.push($scope.tagText);
|
||||
$scope.inputTags = tagArray.join(',');
|
||||
$scope.inputTags = tagArray.join(' ');
|
||||
return $scope.tagText = '';
|
||||
};
|
||||
$scope.deleteTag = function (key) {
|
||||
@@ -610,7 +512,8 @@ app.directive('tagInput', function () {
|
||||
tagArray.splice(key, 1);
|
||||
}
|
||||
}
|
||||
return $scope.inputTags = tagArray.join(',');
|
||||
formCtrl.$setDirty();
|
||||
return $scope.inputTags = tagArray.join(' ');
|
||||
};
|
||||
$scope.$watch('tagText', function (newVal, oldVal) {
|
||||
var tempEl;
|
||||
@@ -623,6 +526,9 @@ app.directive('tagInput', function () {
|
||||
return tempEl.remove();
|
||||
}
|
||||
});
|
||||
element.bind('click', function () {
|
||||
element[0].firstChild.lastChild.focus();
|
||||
});
|
||||
element.bind('keydown', function (e) {
|
||||
var key = e.which;
|
||||
if (key === 9 || key === 13) {
|
||||
@@ -634,7 +540,7 @@ app.directive('tagInput', function () {
|
||||
});
|
||||
element.bind('keyup', function (e) {
|
||||
var key = e.which;
|
||||
if (key === 9 || key === 13 || key === 32 || key === 188) {
|
||||
if (key === 9 || key === 13 || key === 32) {
|
||||
e.preventDefault();
|
||||
return $scope.$apply('addTag()');
|
||||
}
|
||||
@@ -642,9 +548,9 @@ app.directive('tagInput', function () {
|
||||
},
|
||||
template:
|
||||
'<div class="tag-input-container">' +
|
||||
'<div class="input-tag" data-ng-repeat="tag in tagArray()">' +
|
||||
'{{tag}}' +
|
||||
'<div class="delete-tag" data-ng-click="deleteTag($index)">×</div>' +
|
||||
'<div class="btn-group input-tag" data-ng-repeat="tag in tagArray()">' +
|
||||
'<button type="button" class="btn btn-xs btn-primary" disabled>{{ tag }}</button>' +
|
||||
'<button type="button" class="btn btn-xs btn-primary" data-ng-click="deleteTag($index)">×</button>' +
|
||||
'</div>' +
|
||||
'<input type="text" data-ng-model="tagText" ng-blur="addTag()" placeholder="{{placeholder}}"/>' +
|
||||
'</div>'
|
||||
|
||||
+66
-17
@@ -3,18 +3,17 @@
|
||||
/* global angular, $, showdown */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', []);
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies']);
|
||||
|
||||
app.filter('markdown2html', function () {
|
||||
var converter = new showdown.Converter({
|
||||
extensions: [ 'targetblank' ],
|
||||
simplifiedAutoLink: true,
|
||||
strikethrough: true,
|
||||
tables: true
|
||||
tables: true,
|
||||
openLinksInNewWindow: true
|
||||
});
|
||||
|
||||
return function (text) {
|
||||
console.log(text)
|
||||
return converter.makeHtml(text);
|
||||
};
|
||||
});
|
||||
@@ -24,7 +23,41 @@ app.config(function ($sceProvider) {
|
||||
$sceProvider.enabled(false);
|
||||
});
|
||||
|
||||
app.controller('LoginController', ['$scope', '$http', function ($scope, $http) {
|
||||
app.config(['$translateProvider', function ($translateProvider) {
|
||||
$translateProvider.useStaticFilesLoader({
|
||||
prefix: 'translation/',
|
||||
suffix: '.json'
|
||||
});
|
||||
$translateProvider.preferredLanguage('en');
|
||||
$translateProvider.fallbackLanguage('en');
|
||||
}]);
|
||||
|
||||
// Add shorthand "tr" filter to avoid having ot use "translate"
|
||||
// This is a copy of the code at https://github.com/angular-translate/angular-translate/blob/master/src/filter/translate.js
|
||||
// If we find out how to get that function handle somehow dynamically we can use that, otherwise the copy is required
|
||||
function translateFilterFactory($parse, $translate) {
|
||||
var translateFilter = function (translationId, interpolateParams, interpolation, forceLanguage) {
|
||||
if (!angular.isObject(interpolateParams)) {
|
||||
var ctx = this || {
|
||||
'__SCOPE_IS_NOT_AVAILABLE': 'More info at https://github.com/angular/angular.js/commit/8863b9d04c722b278fa93c5d66ad1e578ad6eb1f'
|
||||
};
|
||||
interpolateParams = $parse(interpolateParams)(ctx);
|
||||
}
|
||||
|
||||
return $translate.instant(translationId, interpolateParams, interpolation, forceLanguage);
|
||||
};
|
||||
|
||||
if ($translate.statefulFilter()) {
|
||||
translateFilter.$stateful = true;
|
||||
}
|
||||
|
||||
return translateFilter;
|
||||
}
|
||||
translateFilterFactory.displayName = 'translateFilterFactory';
|
||||
app.filter('tr', translateFilterFactory);
|
||||
|
||||
|
||||
app.controller('LoginController', ['$scope', '$translate', '$http', function ($scope, $translate, $http) {
|
||||
// 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.indexOf('=') === -1 ? [item, true] : [item.slice(0, item.indexOf('=')), item.slice(item.indexOf('=')+1)]; }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
@@ -64,7 +97,12 @@ app.controller('LoginController', ['$scope', '$http', function ($scope, $http) {
|
||||
if (status !== 200) return error();
|
||||
|
||||
localStorage.token = data.accessToken;
|
||||
window.location.href = search.returnTo || '/';
|
||||
|
||||
// prevent redirecting to random domains
|
||||
var returnTo = search.returnTo || '/';
|
||||
if (returnTo.indexOf('/') !== 0) returnTo = '/';
|
||||
|
||||
window.location.href = returnTo;
|
||||
}).error(error);
|
||||
};
|
||||
|
||||
@@ -88,39 +126,40 @@ app.controller('LoginController', ['$scope', '$http', function ($scope, $http) {
|
||||
|
||||
var data = {
|
||||
resetToken: search.resetToken,
|
||||
password: $scope.newPassword
|
||||
password: $scope.newPassword,
|
||||
totpToken: $scope.totpToken
|
||||
};
|
||||
|
||||
function error(status) {
|
||||
console.log('error', status)
|
||||
function error(data, status) {
|
||||
console.log('error', status);
|
||||
$scope.busy = false;
|
||||
|
||||
if (status === 401) $scope.error = 'Invalid reset token';
|
||||
if (status === 401) $scope.error = data.message;
|
||||
else if (status === 409) $scope.error = 'Ask your admin for an invite link first';
|
||||
else $scope.error = 'Unknown error';
|
||||
}
|
||||
|
||||
$http.post(API_ORIGIN + '/api/v1/cloudron/password_reset', data).success(function (data, status) {
|
||||
if (status !== 202) return error(status);
|
||||
if (status !== 202) return error(data, status);
|
||||
|
||||
// set token to autologin
|
||||
localStorage.token = data.accessToken;
|
||||
|
||||
$scope.mode = 'newPasswordDone';
|
||||
}).error(function (data, status) {
|
||||
error(status);
|
||||
error(data, status);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showPasswordReset = function () {
|
||||
window.document.title = 'Password Reset';
|
||||
window.document.title = 'Password Reset Request';
|
||||
$scope.mode = 'passwordReset';
|
||||
$scope.passwordResetIdentifier = '';
|
||||
setTimeout(function () { $('#inputPasswordResetIdentifier').focus(); }, 200);
|
||||
};
|
||||
|
||||
$scope.showLogin = function () {
|
||||
window.document.title = 'Cloudron Login';
|
||||
if ($scope.status) window.document.title = $scope.status.cloudronName + ' Login';
|
||||
$scope.mode = 'login';
|
||||
$scope.error = false;
|
||||
setTimeout(function () { $('#inputUsername').focus(); }, 200);
|
||||
@@ -137,13 +176,23 @@ app.controller('LoginController', ['$scope', '$http', function ($scope, $http) {
|
||||
|
||||
if (status !== 200) return;
|
||||
|
||||
if (data.language) $translate.use(data.language);
|
||||
|
||||
if ($scope.mode === 'login') window.document.title = data.cloudronName + ' Login';
|
||||
$scope.status = data;
|
||||
}).error(function () {
|
||||
$scope.initialized = false;
|
||||
});
|
||||
|
||||
// Init into the correct view
|
||||
if (search.passwordReset) $scope.showPasswordReset();
|
||||
else if (search.resetToken) $scope.showNewPassword();
|
||||
else $scope.showLogin();
|
||||
if (search.passwordReset) {
|
||||
$scope.showPasswordReset();
|
||||
} else if (search.resetToken) {
|
||||
$scope.showNewPassword();
|
||||
} else if (search.accessToken || search.access_token) { // auto-login feature
|
||||
localStorage.token = search.accessToken || search.access_token;
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
$scope.showLogin();
|
||||
}
|
||||
}]);
|
||||
|
||||
+17
-3
@@ -5,9 +5,9 @@
|
||||
/* global $ */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['angular-md5', 'ui-notification']);
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification']);
|
||||
|
||||
app.controller('LogsController', ['$scope', 'Client', function ($scope, Client) {
|
||||
app.controller('LogsController', ['$scope', '$translate', 'Client', function ($scope, $translate, 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.initialized = false;
|
||||
@@ -83,7 +83,15 @@ app.controller('LogsController', ['$scope', 'Client', function ($scope, Client)
|
||||
}
|
||||
|
||||
function select(ids, callback) {
|
||||
if (ids.id) {
|
||||
if (ids.id && ids.id.indexOf('redis:') === 0) {
|
||||
$scope.selected = {
|
||||
name: 'Redis',
|
||||
type: 'service',
|
||||
value: ids.id,
|
||||
url: Client.makeURL('/api/v1/services/' + ids.id + '/logs')
|
||||
};
|
||||
callback();
|
||||
} else if (ids.id) {
|
||||
var BUILT_IN_LOGS = [
|
||||
{ name: 'Box', type: 'platform', value: 'box', url: Client.makeURL('/api/v1/cloudron/logs/box') },
|
||||
{ name: 'Graphite', type: 'service', value: 'graphite', url: Client.makeURL('/api/v1/services/graphite/logs') },
|
||||
@@ -92,8 +100,10 @@ app.controller('LogsController', ['$scope', 'Client', function ($scope, Client)
|
||||
{ name: 'PostgreSQL', type: 'service', value: 'postgresql', url: Client.makeURL('/api/v1/services/postgresql/logs') },
|
||||
{ name: 'Mail', type: 'service', value: 'mail', url: Client.makeURL('/api/v1/services/mail/logs') },
|
||||
{ name: 'Docker', type: 'service', value: 'docker', url: Client.makeURL('/api/v1/services/docker/logs') },
|
||||
{ name: 'Nginx', type: 'service', value: 'nginx', url: Client.makeURL('/api/v1/services/nginx/logs') },
|
||||
{ name: 'Unbound', type: 'service', value: 'unbound', url: Client.makeURL('/api/v1/services/unbound/logs') },
|
||||
{ name: 'SFTP', type: 'service', value: 'sftp', url: Client.makeURL('/api/v1/services/sftp/logs') },
|
||||
{ name: 'TURN/STUN', type: 'service', value: 'turn', url: Client.makeURL('/api/v1/services/turn/logs') },
|
||||
];
|
||||
|
||||
$scope.selected = BUILT_IN_LOGS.find(function (e) { return e.value === ids.id; });
|
||||
@@ -185,4 +195,8 @@ app.controller('LogsController', ['$scope', 'Client', function ($scope, Client)
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
$translate([ 'logs.title' ]).then(function (tr) {
|
||||
if (tr['logs.title'] !== 'logs.title') window.document.title = tr['logs.title'];
|
||||
});
|
||||
}]);
|
||||
|
||||
+54
-27
@@ -3,16 +3,38 @@
|
||||
/* global angular */
|
||||
/* global $ */
|
||||
|
||||
angular.module('Application').controller('MainController', ['$scope', '$route', '$timeout', '$location', 'Client', function ($scope, $route, $timeout, $location, Client) {
|
||||
angular.module('Application').controller('MainController', ['$scope', '$route', '$timeout', '$location', '$interval', 'Client', function ($scope, $route, $timeout, $location, $interval, Client) {
|
||||
$scope.initialized = false; // used to animate the UI
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.installedApps = Client.getInstalledApps();
|
||||
$scope.config = {};
|
||||
$scope.client = Client;
|
||||
$scope.subscription = {};
|
||||
$scope.notifications = [];
|
||||
$scope.notificationCount = 0;
|
||||
$scope.hideNavBarActions = $location.path() === '/logs';
|
||||
|
||||
$scope.reboot = {
|
||||
busy: false,
|
||||
|
||||
show: function () {
|
||||
$scope.reboot.busy = false;
|
||||
$('#rebootModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.reboot.busy = true;
|
||||
|
||||
Client.reboot(function (error) {
|
||||
if (error) return Client.error(error);
|
||||
|
||||
$('#rebootModal').modal('hide');
|
||||
|
||||
// trigger refetch to show offline banner
|
||||
$timeout(function () { Client.getStatus(function () {}); }, 5000);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.isActive = function (url) {
|
||||
if (!$route.current) return false;
|
||||
return $route.current.$$route.originalPath.indexOf(url) === 0;
|
||||
@@ -30,29 +52,28 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
|
||||
// NOTE: this function is exported and called from the appstore.js
|
||||
$scope.updateSubscriptionStatus = function () {
|
||||
if (!Client.getUserInfo().isAtLeastAdmin) return;
|
||||
|
||||
Client.getSubscription(function (error, subscription) {
|
||||
if (error && error.statusCode === 412) return; // ignore if not yet registered
|
||||
if (error && error.statusCode === 402) return; // ignore if not yet registered
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.subscription = subscription;
|
||||
});
|
||||
};
|
||||
|
||||
function refreshNotifications(poll) {
|
||||
Client.getNotifications(false, 1, 100, function (error, results) {
|
||||
if (error) console.error(error);
|
||||
else $scope.notifications = results;
|
||||
function refreshNotifications() {
|
||||
if (!Client.getUserInfo().isAtLeastAdmin) return;
|
||||
|
||||
if (poll) $timeout(refreshNotifications, 60 * 1000);
|
||||
Client.getNotifications({ acknowledged: false }, 1, 100, function (error, results) { // counter maxes out at 100
|
||||
if (error) console.error(error);
|
||||
else $scope.notificationCount = results.length;
|
||||
});
|
||||
}
|
||||
|
||||
// update state of acknowledged notification
|
||||
$scope.notificationAcknowledged = function (notificationId) {
|
||||
// remove notification from list
|
||||
$scope.notifications = $scope.notifications.filter(function (n) { return n.id !== notificationId; });
|
||||
$scope.notificationAcknowledged = function () {
|
||||
if ($scope.notificationCount === 0) return; // already down to 0
|
||||
$scope.notificationCount--;
|
||||
};
|
||||
|
||||
function init() {
|
||||
@@ -68,17 +89,17 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
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.href = '/restore.html' + window.location.search;
|
||||
} else {
|
||||
window.location.href = status.adminFqdn ? '/setup.html' : '/setupdns.html';
|
||||
window.location.href = (status.adminFqdn ? '/setup.html' : '/setupdns.html') + window.location.search;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// support local development with localhost check
|
||||
if (window.location.hostname !== status.adminFqdn && window.location.hostname !== 'localhost') {
|
||||
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.href = '/setupdns.html' + window.location.search;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -99,19 +120,29 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
Client.refreshConfig(function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
Client.refreshInstalledApps(function (error) {
|
||||
Client.refreshAvailableLanguages(function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
// now mark the Client to be ready
|
||||
Client.setReady();
|
||||
Client.refreshInstalledApps(function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
// now mark the Client to be ready
|
||||
Client.setReady();
|
||||
|
||||
$scope.initialized = true;
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
refreshNotifications(true);
|
||||
$scope.initialized = true;
|
||||
|
||||
$scope.updateSubscriptionStatus();
|
||||
if (Client.getConfig().mandatory2FA && !Client.getUserInfo().twoFactorAuthenticationEnabled) {
|
||||
$location.path('/profile').search({ setup2fa: true });
|
||||
return;
|
||||
}
|
||||
|
||||
$interval(refreshNotifications, 60 * 1000);
|
||||
refreshNotifications();
|
||||
|
||||
$scope.updateSubscriptionStatus();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -124,10 +155,6 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
}
|
||||
});
|
||||
|
||||
Client.onReconnect(function () {
|
||||
refreshNotifications(false);
|
||||
});
|
||||
|
||||
init();
|
||||
|
||||
// setup all the dialog focus handling
|
||||
|
||||
+130
-10
@@ -5,7 +5,7 @@
|
||||
/* global $ */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['angular-md5', 'ui-notification', 'ui.bootstrap']);
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification', 'ui.bootstrap']);
|
||||
|
||||
app.filter('zoneName', function () {
|
||||
return function (domain) {
|
||||
@@ -20,9 +20,12 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
$scope.busy = false;
|
||||
$scope.error = {};
|
||||
$scope.message = ''; // progress
|
||||
|
||||
// variables here have to match the import config logic!
|
||||
$scope.provider = '';
|
||||
$scope.bucket = '';
|
||||
$scope.prefix = '';
|
||||
$scope.mountPoint = '';
|
||||
$scope.accessKeyId = '';
|
||||
$scope.secretAccessKey = '';
|
||||
$scope.gcsKey = { keyFileName: '', content: '' };
|
||||
@@ -34,6 +37,22 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
$scope.acceptSelfSignedCerts = false;
|
||||
$scope.format = 'tgz';
|
||||
$scope.advancedVisible = false;
|
||||
$scope.password = '';
|
||||
$scope.encrypted = false; // only used if a backup config contains that flag
|
||||
$scope.setupToken = '';
|
||||
$scope.skipDnsSetup = false;
|
||||
|
||||
$scope.mountOptions = {
|
||||
host: '',
|
||||
remoteDir: '',
|
||||
username: '',
|
||||
password: '',
|
||||
diskPath: '',
|
||||
user: '',
|
||||
seal: false,
|
||||
port: 22,
|
||||
privateKey: ''
|
||||
};
|
||||
|
||||
$scope.sysinfo = {
|
||||
provider: 'generic',
|
||||
@@ -65,6 +84,8 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
{ name: 'Asia Pacific (Sydney)', value: 'ap-southeast-2' },
|
||||
{ name: 'Asia Pacific (Tokyo)', value: 'ap-northeast-1' },
|
||||
{ name: 'Canada (Central)', value: 'ca-central-1' },
|
||||
{ name: 'China (Beijing)', value: 'cn-north-1' },
|
||||
{ name: 'China (Ningxia)', value: 'cn-northwest-1' },
|
||||
{ name: 'EU (Frankfurt)', value: 'eu-central-1' },
|
||||
{ name: 'EU (Ireland)', value: 'eu-west-1' },
|
||||
{ name: 'EU (London)', value: 'eu-west-2' },
|
||||
@@ -82,6 +103,7 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
{ name: 'FRA1', value: 'https://fra1.digitaloceanspaces.com' },
|
||||
{ name: 'NYC3', value: 'https://nyc3.digitaloceanspaces.com' },
|
||||
{ name: 'SFO2', value: 'https://sfo2.digitaloceanspaces.com' },
|
||||
{ name: 'SFO3', value: 'https://sfo3.digitaloceanspaces.com' },
|
||||
{ name: 'SGP1', value: 'https://sgp1.digitaloceanspaces.com' }
|
||||
];
|
||||
|
||||
@@ -101,24 +123,56 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
$scope.linodeRegions = [
|
||||
{ name: 'Newark', value: 'us-east-1.linodeobjects.com', region: 'us-east-1' }, // default
|
||||
{ name: 'Frankfurt', value: 'eu-central-1.linodeobjects.com', region: 'us-east-1' },
|
||||
{ name: 'Singapore', value: 'ap-south-1.linodeobjects.com', region: 'us-east-1' },
|
||||
];
|
||||
|
||||
// note: ovh also has a storage endpoint but that only supports path style access
|
||||
$scope.ovhRegions = [
|
||||
{ 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' },
|
||||
];
|
||||
|
||||
// https://devops.ionos.com/api/s3/
|
||||
$scope.ionosRegions = [
|
||||
{ name: 'DE', value: 'https://s3-de-central.profitbricks.com', region: 's3-de-central' }, // default
|
||||
];
|
||||
|
||||
$scope.vultrRegions = [
|
||||
{ name: 'New Jersey', value: 'https://ewr1.vultrobjects.com', region: 'us-east-1' }, // default
|
||||
];
|
||||
|
||||
$scope.wasabiRegions = [
|
||||
{ name: 'EU Central 1', value: 'https://s3.eu-central-1.wasabisys.com' },
|
||||
{ name: 'US East 1', value: 'https://s3.wasabisys.com' },
|
||||
{ name: 'US East 1', value: 'https://s3.us-east-1.wasabisys.com' },
|
||||
{ name: 'US East 2', value: 'https://s3.us-east-2.wasabisys.com ' },
|
||||
{ name: 'US West 1', value: 'https://s3.us-west-1.wasabisys.com' }
|
||||
];
|
||||
|
||||
$scope.storageProvider = [
|
||||
{ name: 'Amazon S3', value: 's3' },
|
||||
{ name: 'Backblaze B2 (S3 API)', value: 'backblaze-b2' },
|
||||
{ name: 'CIFS Mount', value: 'cifs' },
|
||||
{ name: 'DigitalOcean Spaces', value: 'digitalocean-spaces' },
|
||||
{ name: 'EXT4 Disk', value: 'ext4' },
|
||||
{ name: 'Exoscale SOS', value: 'exoscale-sos' },
|
||||
{ name: 'Filesystem', value: 'filesystem' },
|
||||
{ name: 'Filesystem (Mountpoint)', value: 'mountpoint' }, // legacy
|
||||
{ name: 'Google Cloud Storage', value: 'gcs' },
|
||||
{ name: 'IONOS (Profitbricks)', value: 'ionos-objectstorage' },
|
||||
{ name: 'Linode Object Storage', value: 'linode-objectstorage' },
|
||||
{ name: 'Minio', value: 'minio' },
|
||||
{ name: 'Scaleway Object Storage', value: 'scaleway-objectstorage' },
|
||||
{ name: 'NFS Mount', value: 'nfs' },
|
||||
{ name: 'OVH Object Storage', value: 'ovh-objectstorage' },
|
||||
{ name: 'S3 API Compatible (v4)', value: 's3-v4-compat' },
|
||||
{ name: 'Scaleway Object Storage', value: 'scaleway-objectstorage' },
|
||||
{ name: 'SSHFS Mount', value: 'sshfs' },
|
||||
{ name: 'UpCloud Object Storage', value: 'upcloud-objectstorage' },
|
||||
{ name: 'Vultr Object Storage', value: 'vultr-objectstorage' },
|
||||
{ name: 'Wasabi', value: 'wasabi' }
|
||||
];
|
||||
|
||||
@@ -130,7 +184,12 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
$scope.s3like = function (provider) {
|
||||
return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat' || provider === 'exoscale-sos'
|
||||
|| provider === 'digitalocean-spaces' || provider === 'wasabi' || provider === 'scaleway-objectstorage'
|
||||
|| provider === 'linode-objectstorage';
|
||||
|| provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'backblaze-b2'
|
||||
|| provider === 'ionos-objectstorage' || provider === 'vultr-objectstorage' || provider === 'upcloud-objectstorage';
|
||||
};
|
||||
|
||||
$scope.mountlike = function (provider) {
|
||||
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4';
|
||||
};
|
||||
|
||||
$scope.restore = function () {
|
||||
@@ -139,9 +198,9 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
|
||||
var backupConfig = {
|
||||
provider: $scope.provider,
|
||||
key: $scope.key,
|
||||
format: $scope.format
|
||||
format: $scope.format,
|
||||
};
|
||||
if ($scope.password) backupConfig.password = $scope.password;
|
||||
|
||||
// only set provider specific fields, this will clear them in the db
|
||||
if ($scope.s3like(backupConfig.provider)) {
|
||||
@@ -156,13 +215,14 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
if ($scope.region) backupConfig.region = $scope.region;
|
||||
delete backupConfig.endpoint;
|
||||
} else if (backupConfig.provider === 'minio' || backupConfig.provider === 's3-v4-compat') {
|
||||
backupConfig.region = 'us-east-1';
|
||||
backupConfig.region = backupConfig.region || 'us-east-1';
|
||||
backupConfig.acceptSelfSignedCerts = $scope.acceptSelfSignedCerts;
|
||||
backupConfig.s3ForcePathStyle = true; // might want to expose this in the UI
|
||||
} else if (backupConfig.provider === 'exoscale-sos') {
|
||||
backupConfig.region = 'us-east-1';
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'wasabi') {
|
||||
backupConfig.region = 'us-east-1';
|
||||
backupConfig.region = $scope.wasabiRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'scaleway-objectstorage') {
|
||||
backupConfig.region = $scope.scalewayRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
@@ -170,6 +230,19 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
} else if (backupConfig.provider === 'linode-objectstorage') {
|
||||
backupConfig.region = $scope.linodeRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'ovh-objectstorage') {
|
||||
backupConfig.region = $scope.ovhRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'ionos-objectstorage') {
|
||||
backupConfig.region = $scope.ionosRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'vultr-objectstorage') {
|
||||
backupConfig.region = $scope.vultrRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'upcloud-objectstorage') {
|
||||
var m = /^.*\.(.*)\.upcloudobjects.com$/.exec(backupConfig.endpoint);
|
||||
backupConfig.region = m ? m[1] : 'us-east-1'; // let it fail in validation phase if m is not valid
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'digitalocean-spaces') {
|
||||
backupConfig.region = 'us-east-1';
|
||||
}
|
||||
@@ -193,6 +266,28 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
$scope.busy = false;
|
||||
return;
|
||||
}
|
||||
} else if ($scope.mountlike(backupConfig.provider)) {
|
||||
backupConfig.prefix = $scope.prefix;
|
||||
backupConfig.mountOptions = {};
|
||||
|
||||
if (backupConfig.provider === 'cifs' || backupConfig.provider === 'sshfs' || backupConfig.provider === 'nfs') {
|
||||
backupConfig.mountOptions.host = $scope.mountOptions.host;
|
||||
backupConfig.mountOptions.remoteDir = $scope.mountOptions.remoteDir;
|
||||
|
||||
if (backupConfig.provider === 'cifs') {
|
||||
backupConfig.mountOptions.username = $scope.mountOptions.username;
|
||||
backupConfig.mountOptions.password = $scope.mountOptions.password;
|
||||
backupConfig.mountOptions.seal = $scope.mountOptions.seal;
|
||||
} else if (backupConfig.provider === 'sshfs') {
|
||||
backupConfig.mountOptions.user = $scope.mountOptions.user;
|
||||
backupConfig.mountOptions.port = $scope.mountOptions.port;
|
||||
backupConfig.mountOptions.privateKey = $scope.mountOptions.privateKey;
|
||||
}
|
||||
} else if (backupConfig.provider === 'ext4') {
|
||||
backupConfig.mountOptions.diskPath = $scope.mountOptions.diskPath;
|
||||
} else if (backupConfig.provider === 'mountpoint') {
|
||||
backupConfig.mountPoint = $scope.mountPoint;
|
||||
}
|
||||
} else if (backupConfig.provider === 'filesystem') {
|
||||
backupConfig.backupFolder = $scope.backupFolder;
|
||||
}
|
||||
@@ -228,7 +323,7 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
sysinfoConfig.ifname = $scope.sysinfo.ifname;
|
||||
}
|
||||
|
||||
Client.restore(backupConfig, $scope.backupId.replace(/\.tar\.gz(\.enc)?$/, ''), version ? version[1] : '', sysinfoConfig, function (error) {
|
||||
Client.restore(backupConfig, $scope.backupId.replace(/\.tar\.gz(\.enc)?$/, ''), version ? version[1] : '', sysinfoConfig, $scope.skipDnsSetup, $scope.setupToken, function (error) {
|
||||
$scope.busy = false;
|
||||
|
||||
if (error) {
|
||||
@@ -288,7 +383,7 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.message = status.restore.message;
|
||||
if (!error) $scope.message = status.restore.message;
|
||||
|
||||
setTimeout(waitForRestore, 5000);
|
||||
});
|
||||
@@ -312,6 +407,29 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
|
||||
document.getElementById('gcsKeyFileInput').onchange = readFileLocally($scope.gcsKey, 'content', 'keyFileName');
|
||||
|
||||
document.getElementById('backupConfigFileInput').onchange = function (event) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (result) {
|
||||
if (!result.target || !result.target.result) return console.error('Unable to read backup config');
|
||||
|
||||
var backupConfig;
|
||||
try {
|
||||
backupConfig = JSON.parse(result.target.result);
|
||||
} catch (e) {
|
||||
console.error('Unable to parse backup config');
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.$apply(function () {
|
||||
// we assume property names match here, this does not yet work for gcs keys
|
||||
Object.keys(backupConfig).forEach(function (k) {
|
||||
if (k in $scope) $scope[k] = backupConfig[k];
|
||||
});
|
||||
});
|
||||
};
|
||||
reader.readAsText(event.target.files[0]);
|
||||
};
|
||||
|
||||
function init() {
|
||||
Client.getStatus(function (error, status) {
|
||||
if (error) return Client.initError(error, init);
|
||||
@@ -325,7 +443,9 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.status = status;
|
||||
$scope.instanceId = search.instanceId;
|
||||
$scope.setupToken = search.setupToken;
|
||||
$scope.initialized = true;
|
||||
});
|
||||
}
|
||||
|
||||
+76
-50
@@ -4,78 +4,104 @@
|
||||
/* global $ */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['angular-md5', 'ui-notification', 'ui.bootstrap']);
|
||||
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.busy = false;
|
||||
$scope.account = {
|
||||
email: '',
|
||||
displayName: '',
|
||||
requireEmail: false,
|
||||
username: '',
|
||||
password: ''
|
||||
};
|
||||
$scope.error = null;
|
||||
$scope.provider = '';
|
||||
$scope.apiServerOrigin = '';
|
||||
$scope.webServerOrigin = '';
|
||||
$scope.setupToken = '';
|
||||
|
||||
$scope.activateCloudron = function () {
|
||||
$scope.busy = true;
|
||||
$scope.error = null;
|
||||
$scope.owner = {
|
||||
error: null,
|
||||
busy: false,
|
||||
|
||||
Client.createAdmin($scope.account.username, $scope.account.password, $scope.account.email, $scope.account.displayName, function (error) {
|
||||
if (error && error.statusCode === 400) {
|
||||
$scope.busy = false;
|
||||
$scope.error = { username: error.message };
|
||||
$scope.account.username = '';
|
||||
$scope.setupForm.username.$setPristine();
|
||||
setTimeout(function () { $('#inputUsername').focus(); }, 200);
|
||||
return;
|
||||
} else if (error) {
|
||||
$scope.busy = false;
|
||||
console.error('Internal error', error);
|
||||
$scope.error = { generic: error.message };
|
||||
return;
|
||||
}
|
||||
email: '',
|
||||
displayName: '',
|
||||
username: '',
|
||||
password: '',
|
||||
|
||||
window.location.href = '/';
|
||||
});
|
||||
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;
|
||||
$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');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// if we are here from the ip first go to the real domain if already setup
|
||||
if (status.adminFqdn && status.adminFqdn !== window.location.hostname) {
|
||||
window.location.href = 'https://' + status.adminFqdn + '/setup.html';
|
||||
return;
|
||||
}
|
||||
|
||||
// if we don't have a domain yet, first go to domain setup
|
||||
if (!status.adminFqdn) {
|
||||
window.location.href = '/setupdns.html';
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.activated) {
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function setView(view) {
|
||||
if (view === 'finished') {
|
||||
$scope.view = 'finished';
|
||||
} else {
|
||||
$scope.view = 'owner';
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
Client.getStatus(function (error, status) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
// if we are here from the ip first go to the real domain if already setup
|
||||
if (status.adminFqdn && status.adminFqdn !== window.location.hostname) {
|
||||
window.location.href = 'https://' + status.adminFqdn + '/setup.html';
|
||||
return;
|
||||
}
|
||||
redirectIfNeeded(status);
|
||||
setView(search.view);
|
||||
|
||||
// if we don't have a domain yet, first go to domain setup
|
||||
if (!status.adminFqdn) {
|
||||
window.location.href = '/setupdns.html';
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.activated) {
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.account.email = search.email || $scope.account.email;
|
||||
$scope.account.displayName = search.displayName || $scope.account.displayName;
|
||||
$scope.account.requireEmail = !search.email;
|
||||
$scope.provider = status.provider;
|
||||
$scope.apiServerOrigin = status.apiServerOrigin;
|
||||
$scope.webServerOrigin = status.webServerOrigin;
|
||||
|
||||
$scope.setupToken = search.setupToken;
|
||||
$scope.initialized = true;
|
||||
|
||||
// Ensure we have a good autofocus
|
||||
|
||||
+61
-14
@@ -3,14 +3,14 @@
|
||||
/* global angular, $, showdown */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', []);
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies']);
|
||||
|
||||
app.filter('markdown2html', function () {
|
||||
var converter = new showdown.Converter({
|
||||
extensions: [ 'targetblank' ],
|
||||
simplifiedAutoLink: true,
|
||||
strikethrough: true,
|
||||
tables: true
|
||||
tables: true,
|
||||
openLinksInNewWindow: true
|
||||
});
|
||||
|
||||
return function (text) {
|
||||
@@ -23,7 +23,44 @@ app.config(function ($sceProvider) {
|
||||
$sceProvider.enabled(false);
|
||||
});
|
||||
|
||||
app.controller('SetupAccountController', ['$scope', '$http', function ($scope, $http) {
|
||||
app.config(['$translateProvider', function ($translateProvider) {
|
||||
$translateProvider.useStaticFilesLoader({
|
||||
prefix: 'translation/',
|
||||
suffix: '.json'
|
||||
});
|
||||
$translateProvider.useLocalStorage();
|
||||
$translateProvider.preferredLanguage('en');
|
||||
$translateProvider.fallbackLanguage('en');
|
||||
}]);
|
||||
|
||||
// Add shorthand "tr" filter to avoid having ot use "translate"
|
||||
// This is a copy of the code at https://github.com/angular-translate/angular-translate/blob/master/src/filter/translate.js
|
||||
// If we find out how to get that function handle somehow dynamically we can use that, otherwise the copy is required
|
||||
function translateFilterFactory($parse, $translate) {
|
||||
|
||||
'use strict';
|
||||
|
||||
var translateFilter = function (translationId, interpolateParams, interpolation, forceLanguage) {
|
||||
if (!angular.isObject(interpolateParams)) {
|
||||
var ctx = this || {
|
||||
'__SCOPE_IS_NOT_AVAILABLE': 'More info at https://github.com/angular/angular.js/commit/8863b9d04c722b278fa93c5d66ad1e578ad6eb1f'
|
||||
};
|
||||
interpolateParams = $parse(interpolateParams)(ctx);
|
||||
}
|
||||
|
||||
return $translate.instant(translationId, interpolateParams, interpolation, forceLanguage);
|
||||
};
|
||||
|
||||
if ($translate.statefulFilter()) {
|
||||
translateFilter.$stateful = true;
|
||||
}
|
||||
|
||||
return translateFilter;
|
||||
}
|
||||
translateFilterFactory.displayName = 'translateFilterFactory';
|
||||
app.filter('tr', translateFilterFactory);
|
||||
|
||||
app.controller('SetupAccountController', ['$scope', '$translate', '$http', function ($scope, $translate, $http) {
|
||||
// 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.indexOf('=') === -1 ? [item, true] : [item.slice(0, item.indexOf('=')), item.slice(item.indexOf('=')+1)]; }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
@@ -35,6 +72,7 @@ app.controller('SetupAccountController', ['$scope', '$http', function ($scope, $
|
||||
$scope.view = 'setup';
|
||||
$scope.status = null;
|
||||
|
||||
$scope.profileLocked = !!search.profileLocked;
|
||||
$scope.existingUsername = !!search.username;
|
||||
$scope.username = search.username || '';
|
||||
$scope.displayName = search.displayName || '';
|
||||
@@ -46,13 +84,15 @@ app.controller('SetupAccountController', ['$scope', '$http', function ($scope, $
|
||||
$scope.error = null;
|
||||
|
||||
var data = {
|
||||
resetToken: search.resetToken,
|
||||
email: search.email,
|
||||
username: $scope.username,
|
||||
displayName: $scope.displayName,
|
||||
inviteToken: search.inviteToken,
|
||||
password: $scope.password
|
||||
};
|
||||
|
||||
if (!$scope.profileLocked) {
|
||||
if (!$scope.existingUsername) data.username = $scope.username;
|
||||
data.displayName = $scope.displayName;
|
||||
}
|
||||
|
||||
function error(data, status) {
|
||||
$scope.busy = false;
|
||||
|
||||
@@ -89,13 +129,20 @@ app.controller('SetupAccountController', ['$scope', '$http', function ($scope, $
|
||||
}).error(error);
|
||||
};
|
||||
|
||||
$http.get(API_ORIGIN + '/api/v1/cloudron/status').success(function (data, status) {
|
||||
if (!$scope.existingUsername && $scope.profileLocked) {
|
||||
$scope.view = 'noUsername';
|
||||
$scope.initialized = true;
|
||||
} else {
|
||||
$http.get(API_ORIGIN + '/api/v1/cloudron/status').success(function (data, status) {
|
||||
$scope.initialized = true;
|
||||
|
||||
if (status !== 200) return;
|
||||
if (status !== 200) return;
|
||||
|
||||
$scope.status = data;
|
||||
}).error(function () {
|
||||
$scope.initialized = false;
|
||||
});
|
||||
if (data.language) $translate.use(data.language);
|
||||
|
||||
$scope.status = data;
|
||||
}).error(function () {
|
||||
$scope.initialized = false;
|
||||
});
|
||||
}
|
||||
}]);
|
||||
|
||||
+50
-15
@@ -1,9 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
/* global $, tld, angular */
|
||||
/* global $, tld, angular, Clipboard */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['angular-md5', 'ui-notification', 'ui.bootstrap']);
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification', 'ui.bootstrap']);
|
||||
|
||||
app.filter('zoneName', function () {
|
||||
return function (domain) {
|
||||
@@ -21,9 +21,11 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
$scope.instanceId = '';
|
||||
$scope.isDomain = false;
|
||||
$scope.isSubdomain = false;
|
||||
$scope.hyphenatedSubdomains = false;
|
||||
$scope.advancedVisible = false;
|
||||
$scope.webServerOrigin = '';
|
||||
$scope.clipboardDone = false;
|
||||
$scope.search = window.location.search;
|
||||
$scope.setupToken = '';
|
||||
|
||||
$scope.tlsProvider = [
|
||||
{ name: 'Let\'s Encrypt Prod', value: 'letsencrypt-prod' },
|
||||
@@ -83,8 +85,11 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
{ name: 'Gandi LiveDNS', value: 'gandi' },
|
||||
{ name: 'GoDaddy', value: 'godaddy' },
|
||||
{ name: 'Google Cloud DNS', value: 'gcdns' },
|
||||
{ name: 'Linode', value: 'linode' },
|
||||
{ name: 'Name.com', value: 'namecom' },
|
||||
{ name: 'Namecheap', value: 'namecheap' },
|
||||
{ name: 'Netcup', value: 'netcup' },
|
||||
{ name: 'Vultr', value: 'vultr' },
|
||||
{ name: 'Wildcard', value: 'wildcard' },
|
||||
{ name: 'Manual (not recommended)', value: 'manual' },
|
||||
{ name: 'No-op (only for development)', value: 'noop' }
|
||||
@@ -102,16 +107,20 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
cloudflareTokenType: 'GlobalApiKey',
|
||||
godaddyApiKey: '',
|
||||
godaddyApiSecret: '',
|
||||
linodeToken: '',
|
||||
vultrToken: '',
|
||||
nameComUsername: '',
|
||||
nameComToken: '',
|
||||
namecheapUsername: '',
|
||||
namecheapApiKey: '',
|
||||
netcupCustomerNumber: '',
|
||||
netcupApiKey: '',
|
||||
netcupApiPassword: '',
|
||||
provider: 'route53',
|
||||
zoneName: '',
|
||||
tlsConfig: {
|
||||
provider: 'letsencrypt-prod-wildcard'
|
||||
},
|
||||
hyphenatedSubdomains: false
|
||||
}
|
||||
};
|
||||
|
||||
$scope.setDefaultTlsProvider = function () {
|
||||
@@ -149,9 +158,7 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
|
||||
var provider = $scope.dnsCredentials.provider;
|
||||
|
||||
var config = {
|
||||
hyphenatedSubdomains: $scope.dnsCredentials.hyphenatedSubdomains
|
||||
};
|
||||
var config = {};
|
||||
|
||||
if (provider === 'route53') {
|
||||
config.accessKeyId = $scope.dnsCredentials.accessKeyId;
|
||||
@@ -184,12 +191,20 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
config.email = $scope.dnsCredentials.cloudflareEmail;
|
||||
config.token = $scope.dnsCredentials.cloudflareToken;
|
||||
config.tokenType = $scope.dnsCredentials.cloudflareTokenType;
|
||||
} else if (provider === 'linode') {
|
||||
config.token = $scope.dnsCredentials.linodeToken;
|
||||
} 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;
|
||||
}
|
||||
|
||||
var tlsConfig = {
|
||||
@@ -211,7 +226,7 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
}
|
||||
|
||||
var data = {
|
||||
dnsConfig: {
|
||||
domainConfig: {
|
||||
domain: $scope.dnsCredentials.domain,
|
||||
zoneName: $scope.dnsCredentials.zoneName,
|
||||
provider: provider,
|
||||
@@ -219,14 +234,19 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
tlsConfig: tlsConfig
|
||||
},
|
||||
sysinfoConfig: sysinfoConfig,
|
||||
providerToken: $scope.instanceId
|
||||
providerToken: $scope.instanceId,
|
||||
setupToken: $scope.setupToken
|
||||
};
|
||||
|
||||
Client.setup(data, function (error) {
|
||||
if (error) {
|
||||
$scope.dnsCredentials.busy = false;
|
||||
if (error.statusCode === 422) {
|
||||
$scope.error.ami = error.message;
|
||||
if (provider === 'ami') {
|
||||
$scope.error.ami = error.message;
|
||||
} else {
|
||||
$scope.error.setup = error.message;
|
||||
}
|
||||
} else {
|
||||
$scope.error.dnsCredentials = error.message;
|
||||
}
|
||||
@@ -247,7 +267,7 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
$scope.state = 'initialized';
|
||||
$scope.dnsCredentials.busy = false;
|
||||
} else { // proceed to activation
|
||||
window.location.href = 'https://' + status.adminFqdn + '/setup.html';
|
||||
window.location.href = 'https://' + status.adminFqdn + '/setup.html' + (window.location.search);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -270,11 +290,20 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
// domain is currently like a lock flag
|
||||
if (status.adminFqdn) return waitForDnsSetup();
|
||||
|
||||
if (status.provider === 'digitalocean' || status.provider === 'digitalocean-mp') $scope.dnsCredentials.provider = 'digitalocean';
|
||||
if (status.provider === 'gce') $scope.dnsCredentials.provider = 'gcdns';
|
||||
if (status.provider === 'ami') $scope.dnsCredentials.provider = 'route53';
|
||||
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.webServerOrigin = status.webServerOrigin;
|
||||
$scope.state = 'initialized';
|
||||
@@ -283,5 +312,11 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
});
|
||||
}
|
||||
|
||||
var clipboard = new Clipboard('.clipboard');
|
||||
clipboard.on('success', function () {
|
||||
$scope.$apply(function () { $scope.clipboardDone = true; });
|
||||
$timeout(function () { $scope.clipboardDone = false; }, 5000);
|
||||
});
|
||||
|
||||
initialize();
|
||||
}]);
|
||||
|
||||
+25
-13
@@ -1,14 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular */
|
||||
/* global $ */
|
||||
/* global Terminal */
|
||||
/* global ISTATES */
|
||||
/* global angular, $, Terminal, AttachAddon, FitAddon, ISTATES */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['angular-md5', 'ui-notification']);
|
||||
angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification']);
|
||||
|
||||
app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client', function ($scope, $timeout, $location, Client) {
|
||||
angular.module('Application').controller('TerminalController', ['$scope', '$translate', '$timeout', '$location', 'Client', function ($scope, $translate, $timeout, $location, 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.config = Client.getConfig();
|
||||
@@ -18,6 +15,7 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
|
||||
$scope.selected = '';
|
||||
$scope.terminal = null;
|
||||
$scope.terminalSocket = null;
|
||||
$scope.fitAddon = null;
|
||||
$scope.restartAppBusy = false;
|
||||
$scope.appBusy = false;
|
||||
$scope.selectedAppInfo = null;
|
||||
@@ -110,7 +108,7 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
|
||||
|
||||
function reset() {
|
||||
if ($scope.terminal) {
|
||||
$scope.terminal.destroy();
|
||||
$scope.terminal.dispose();
|
||||
$scope.terminal = null;
|
||||
}
|
||||
|
||||
@@ -151,9 +149,10 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
|
||||
function createTerminalSocket() {
|
||||
try {
|
||||
// websocket cannot use relative urls
|
||||
var url = Client.apiOrigin.replace('https', 'wss') + '/api/v1/apps/' + $scope.selected.value + '/execws?tty=true&rows=' + $scope.terminal.rows + '&columns=' + $scope.terminal.cols + '&access_token=' + Client.getToken();
|
||||
var cmd = JSON.stringify([ '/bin/bash' ]);
|
||||
var url = Client.apiOrigin.replace('https', 'wss') + '/api/v1/apps/' + $scope.selected.value + '/execws?tty=true&rows=' + $scope.terminal.rows + '&columns=' + $scope.terminal.cols + '&access_token=' + Client.getToken() + '&cmd=' + cmd;
|
||||
$scope.terminalSocket = new WebSocket(url);
|
||||
$scope.terminal.attach($scope.terminalSocket);
|
||||
$scope.terminal.loadAddon(new AttachAddon.AttachAddon($scope.terminalSocket));
|
||||
|
||||
$scope.terminalSocket.onclose = function () {
|
||||
// retry in one second
|
||||
@@ -192,7 +191,11 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
|
||||
$scope.schedulerTasks = result.manifest.addons.scheduler ? Object.keys(result.manifest.addons.scheduler).map(function (k) { return { name: k, command: result.manifest.addons.scheduler[k].command }; }) : [];
|
||||
|
||||
$scope.terminal = new Terminal();
|
||||
$scope.terminal.open(document.querySelector('#terminalContainer'), true);
|
||||
|
||||
$scope.fitAddon = new FitAddon.FitAddon();
|
||||
$scope.terminal.loadAddon($scope.fitAddon);
|
||||
|
||||
$scope.terminal.open(document.querySelector('#terminalContainer'));
|
||||
|
||||
window.terminal = $scope.terminal;
|
||||
|
||||
@@ -207,14 +210,19 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
|
||||
// we have to give it some time to setup the terminal to make it fit, there is no event unfortunately
|
||||
setTimeout(function () {
|
||||
if (!$scope.terminal) return;
|
||||
$scope.terminal.fit();
|
||||
|
||||
// this is here so that the text wraps correctly after the fit!
|
||||
var YELLOW = '\u001b[33m'; // https://gist.github.com/dainkaplan/4651352
|
||||
var NC = '\u001b[0m';
|
||||
$scope.terminal.writeln(YELLOW + 'If you resize the browser window, press Ctrl+D to start a new session with the current size.' + NC);
|
||||
|
||||
createTerminalSocket(); // create exec container after we fit() since we cannot resize exec container post-creation
|
||||
// we have to first write something on reconnect after app restart..not sure why
|
||||
$scope.fitAddon.fit();
|
||||
|
||||
// create exec container after we fit() since we cannot resize exec container post-creation
|
||||
createTerminalSocket();
|
||||
|
||||
$scope.terminal.focus();
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
@@ -299,7 +307,7 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
|
||||
});
|
||||
|
||||
window.addEventListener('resize', function () {
|
||||
if ($scope.terminal) $scope.terminal.fit();
|
||||
if ($scope.fitAddon) $scope.fitAddon.fit();
|
||||
});
|
||||
|
||||
Client.getStatus(function (error, status) {
|
||||
@@ -349,6 +357,10 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
|
||||
});
|
||||
});
|
||||
|
||||
$translate([ 'terminal.title' ]).then(function (tr) {
|
||||
if (tr['terminal.title'] !== 'terminal.title') window.document.title = tr['terminal.title'];
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['downloadFileModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/* This file contains helpers which should not be part of client.js */
|
||||
|
||||
angular.module('Application').directive('passwordReveal', function () {
|
||||
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>';
|
||||
|
||||
return {
|
||||
link: function (scope, elements) {
|
||||
var element = elements[0];
|
||||
|
||||
if (!element.parentNode) {
|
||||
console.error('Wrong password-reveal directive usage. Element has no parent.');
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
});
|
||||
+54
-41
@@ -5,44 +5,54 @@
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src <%= apiOrigin %> 'unsafe-inline' 'unsafe-eval' 'self'; img-src <%= apiOrigin %> 'self'" />
|
||||
|
||||
<title>Cloudron Login</title>
|
||||
<!-- this gets changed once we get the status (because angular has not loaded yet, we see template string for a flash) -->
|
||||
<title>‎</title>
|
||||
|
||||
<link id="favicon" href="<%= apiOrigin %>/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css">
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css?<%= revision %>">
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.min.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>
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-1.6.4.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-target-blank.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></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.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js?<%= revision %>"></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>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/autofill-event.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>
|
||||
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.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/login.js"></script>
|
||||
<script type="text/javascript" src="/js/login.js?<%= revision %>"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body ng-app="Application" ng-controller="LoginController">
|
||||
|
||||
<div class="layout-root" ng-show="initialized">
|
||||
<div class="layout-root ng-cloak" ng-show="initialized">
|
||||
|
||||
<div class="layout-content" ng-show="mode === 'login'">
|
||||
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
|
||||
@@ -50,33 +60,33 @@
|
||||
<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/>
|
||||
<h1><small>Login to</small> {{ status.cloudronName || 'Cloudron' }}</h1>
|
||||
<h1><small>{{ 'login.loginTo' | tr }}</small> {{ status.cloudronName || 'Cloudron' }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4 class="has-error" ng-show="error">Incorrect username or password</h4>
|
||||
<h4 class="has-error" ng-show="error">{{ 'login.errorIncorrectCredentials' | tr }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form name="loginForm" ng-submit="onLogin()">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputUsername">Username</label>
|
||||
<label class="control-label" for="inputUsername">{{ 'login.username' | tr }}</label>
|
||||
<input type="text" class="form-control" id="inputUsername" name="username" ng-model="username" ng-disabled="busy" autofocus required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputPassword">Password</label>
|
||||
<input type="password" class="form-control" name="password" id="inputPassword" ng-model="password" ng-disabled="busy" required>
|
||||
<label class="control-label" for="inputPassword">{{ 'login.password' | tr }}</label>
|
||||
<input type="password" class="form-control" name="password" id="inputPassword" ng-model="password" ng-disabled="busy" required password-reveal>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputPassword">2FA Token (if enabled)</label>
|
||||
<label class="control-label" for="inputTotpToken">{{ 'login.2faToken' | tr }}</label>
|
||||
<input type="text" class="form-control" name="totpToken" id="inputTotpToken" ng-model="totpToken" ng-disabled="busy" value="">
|
||||
</div>
|
||||
<button class="btn btn-primary btn-outline pull-right" type="submit" ng-disabled="busy || loginForm.$invalid"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> Sign in</button>
|
||||
<button class="btn btn-primary btn-outline pull-right" type="submit" ng-disabled="busy || loginForm.$invalid"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'login.signInAction' | tr }}</button>
|
||||
</form>
|
||||
<a ng-href="" class="hand" ng-click="showPasswordReset()">Reset password</a>
|
||||
<a ng-href="" class="hand" ng-click="showPasswordReset()">{{ 'login.resetPasswordAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,7 +98,7 @@
|
||||
<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>Password reset</h2>
|
||||
<h2>{{ 'passwordReset.title' | tr }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
@@ -96,13 +106,13 @@
|
||||
<div class="col-md-12">
|
||||
<form name="passwordResetForm" ng-submit="onPasswordReset()">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputPasswordResetIdentifier">Username or Email</label>
|
||||
<label class="control-label" for="inputPasswordResetIdentifier">{{ 'passwordReset.usernameOrEmail' | tr }}</label>
|
||||
<input type="text" class="form-control" id="inputPasswordResetIdentifier" name="passwordResetIdentifier" ng-model="passwordResetIdentifier" ng-disabled="busy" autofocus required>
|
||||
</div>
|
||||
<br/>
|
||||
<button class="btn btn-primary btn-outline pull-right" type="submit" ng-disabled="busy || passwordResetForm.$invalid"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> Reset</button>
|
||||
<button class="btn btn-primary btn-outline pull-right" type="submit" ng-disabled="busy || passwordResetForm.$invalid"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'passwordReset.resetAction' | tr }}</button>
|
||||
</form>
|
||||
<a ng-href="" class="hand" ng-click="showLogin()">Back to login</a>
|
||||
<a ng-href="" class="hand" ng-click="showLogin()">{{ 'passwordReset.backToLoginAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,9 +124,9 @@
|
||||
<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>Password reset email sent</h2>
|
||||
<h2>{{ 'passwordReset.emailSent.title' | tr }}</h2>
|
||||
<br/>
|
||||
<button class="btn btn-primary" ng-click="showLogin()">Back to login</button>
|
||||
<button class="btn btn-primary" ng-click="showLogin()">{{ 'passwordReset.backToLoginAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -128,7 +138,7 @@
|
||||
<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>Set new password</h2>
|
||||
<h2>{{ 'passwordReset.newPassword.title' | tr }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
@@ -143,23 +153,27 @@
|
||||
<form name="newPasswordForm" ng-submit="onNewPassword()">
|
||||
<input type="password" style="display: none;"/>
|
||||
<div class="form-group" ng-class="{ 'has-error': newPasswordForm.newPassword.$dirty && newPasswordForm.newPassword.$invalid }">
|
||||
<label class="control-label" for="inputNewPassword">New Password</label>
|
||||
<label class="control-label" for="inputNewPassword">{{ 'passwordReset.newPassword.password' | tr }}</label>
|
||||
<div class="control-label" ng-show="newPasswordForm.newPassword.$dirty && newPasswordForm.newPassword.$invalid">
|
||||
<small ng-show="newPasswordForm.newPassword.$dirty && newPasswordForm.newPassword.$invalid">Password must be atleast 8 and at most 265 characters</small>
|
||||
<small ng-show="newPasswordForm.newPassword.$dirty && newPasswordForm.newPassword.$invalid">{{ 'passwordReset.newPassword.errorLength' | tr }}</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputNewPassword" ng-model="newPassword" name="newPassword" ng-minlength="8" ng-maxlength="256" autofocus required>
|
||||
<input type="password" class="form-control" id="inputNewPassword" ng-model="newPassword" name="newPassword" ng-minlength="8" ng-maxlength="256" autofocus required password-reveal>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': newPasswordForm.newPasswordRepeat.$dirty && (newPassword !== newPasswordRepeat) }">
|
||||
<label class="control-label" for="inputNewPasswordRepeat">Repeat Password</label>
|
||||
<label class="control-label" for="inputNewPasswordRepeat">{{ 'passwordReset.newPassword.passwordRepeat' | tr }}</label>
|
||||
<div class="control-label" ng-show="newPasswordForm.newPasswordRepeat.$dirty && (newPassword !== newPasswordRepeat)">
|
||||
<small ng-show="newPasswordForm.newPasswordRepeat.$dirty && (newPassword !== newPasswordRepeat)">Passwords don't match</small>
|
||||
<small ng-show="newPasswordForm.newPasswordRepeat.$dirty && (newPassword !== newPasswordRepeat)">{{ 'passwordReset.newPassword.errorMismatch' | tr }}</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputNewPasswordRepeat" ng-model="newPasswordRepeat" name="newPasswordRepeat" required>
|
||||
<input type="password" class="form-control" id="inputNewPasswordRepeat" ng-model="newPasswordRepeat" name="newPasswordRepeat" required password-reveal>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputPasswordResetTotpToken">{{ 'login.2faToken' | tr }}</label>
|
||||
<input type="text" class="form-control" name="passwordResetTotpToken" id="inputPasswordResetTotpToken" ng-model="totpToken" ng-disabled="busy" value="">
|
||||
</div>
|
||||
<br/>
|
||||
<button class="btn btn-primary btn-outline pull-right" type="submit" ng-disabled="busy || newPasswordForm.$invalid || newPassword !== newPasswordRepeat"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> Submit</button>
|
||||
<button class="btn btn-primary btn-outline pull-right" type="submit" ng-disabled="busy || newPasswordForm.$invalid || newPassword !== newPasswordRepeat"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'passwordReset.passwordChanged.submitAction' | tr }}</button>
|
||||
</form>
|
||||
<a ng-href="" class="hand" ng-click="showLogin()">Back to login</a>
|
||||
<a ng-href="" class="hand" ng-click="showLogin()">{{ 'passwordReset.backToLoginAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -171,17 +185,16 @@
|
||||
<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>Password changed</h2>
|
||||
<h2>{{ 'passwordReset.success.title' | tr }}</h2>
|
||||
<br/>
|
||||
<a href="/" class="btn btn-primary">Open Dashboard</a>
|
||||
<a href="/" class="btn btn-primary">{{ 'passwordReset.success.openDashboardAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-center ng-cloak">
|
||||
<footer class="text-center">
|
||||
<span class="text-muted" ng-bind-html="status.footer | markdown2html"></span>
|
||||
<span class="version">v{{status.version}}</span>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
+58
-47
@@ -1,72 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html ng-app="Application" ng-controller="LogsController">
|
||||
<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" />
|
||||
|
||||
<!-- this gets changed once we get the config (because angular has not loaded yet, we see template string for a flash) -->
|
||||
<title> Logs </title>
|
||||
<title> Logs </title>
|
||||
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
<link rel="apple-touch-icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar">
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
<link rel="apple-touch-icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar">
|
||||
|
||||
<!-- CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.css"/>
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css">
|
||||
<!-- CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.css?<%= revision %>"/>
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css?<%= revision %>">
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.min.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>
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js"></script>
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js?<%= revision %>"></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-animate.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-base64.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-sanitize.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script>
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.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>
|
||||
<!-- Angularjs scripts -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-animate.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-base64.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-sanitize.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js?<%= revision %>"></script>
|
||||
|
||||
<!-- colors -->
|
||||
<script type="text/javascript" src="/3rdparty/js/colors.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?<%= revision %>"></script>
|
||||
|
||||
<!-- moment -->
|
||||
<script type="text/javascript" src="/3rdparty/js/moment.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>
|
||||
|
||||
<!-- Main Application -->
|
||||
<script type="text/javascript" src="/js/logs.js"></script>
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- colors -->
|
||||
<script type="text/javascript" src="/3rdparty/js/colors.js?<%= revision %>"></script>
|
||||
|
||||
<!-- moment -->
|
||||
<script type="text/javascript" src="/3rdparty/js/moment.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Main Application -->
|
||||
<script type="text/javascript" src="/js/logs.js?<%= revision %>"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="logs">
|
||||
|
||||
<a class="offline-banner animateMe" ng-show="client.offline" ng-cloak href="https://cloudron.io/documentation/troubleshooting/" target="_blank"><i class="fa fa-circle-notch fa-spin"></i> Cloudron is offline. Reconnecting...</a>
|
||||
<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> {{ 'main.offline' | tr }}</a>
|
||||
|
||||
<div class="animateMe ng-hide layout-root" ng-show="initialized">
|
||||
|
||||
<div class="logs-controls">
|
||||
<h3 style="display: inline-block;">{{ selected.name }} Logs</h3>
|
||||
|
||||
<!-- logs actions -->
|
||||
<div class="pull-right">
|
||||
<a class="btn btn-primary" ng-href="{{ '/terminal.html?id=' + selected.value }}" target="_blank" ng-show="selected.type === 'app'"><i class="fa fa-terminal"></i> Terminal</a>
|
||||
<a class="btn btn-primary" ng-click="clear()"><i class="fa fa-trash"></i> Clear View</a>
|
||||
<a class="btn btn-primary" ng-href="{{ selected.url }}&format=short&lines=-1"><i class="fa fa-download"></i> Download Full Logs</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="logs-container"></div>
|
||||
<div class="animateMe ng-hide layout-root" ng-show="initialized">
|
||||
<div class="logs-controls">
|
||||
<h3 style="display: inline-block;">{{ selected.name }}</h3>
|
||||
|
||||
<!-- logs actions -->
|
||||
<div class="pull-right">
|
||||
<a class="btn btn-primary" ng-href="{{ '/terminal.html?id=' + selected.value }}" target="_blank" ng-show="selected.type === 'app'"><i class="fa fa-terminal"></i> {{ 'terminal.title' | tr }}</a>
|
||||
<a class="btn btn-primary" ng-href="{{ '/filemanager.html?type=app&id=' + selected.value }}" target="_blank" ng-show="selected.type === 'app'"><i class="fas fa-folder"></i> {{ 'filemanager.title' | tr }}</a>
|
||||
<a class="btn btn-primary" ng-click="clear()"><i class="fa fa-trash"></i> {{ 'logs.clear' | tr }}</a>
|
||||
<a class="btn btn-primary" ng-href="{{ selected.url }}&format=short&lines=-1"><i class="fa fa-download"></i> {{ 'logs.download' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="logs-container"></div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<!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 - Not Found</title>
|
||||
|
||||
<!-- Use static style as we can't include local stylesheets -->
|
||||
<style>
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: white;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-family: "Roboto","Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.846;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #2196f3;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #0a6ebd;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<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'
|
||||
+ ' but Cloudron has no app configured for this domain.';
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="content">
|
||||
<p id="message"></p>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+123
-12
@@ -11,18 +11,22 @@
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css">
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.min.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>
|
||||
@@ -33,6 +37,15 @@
|
||||
<!-- 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/restore.js"></script>
|
||||
|
||||
@@ -40,7 +53,7 @@
|
||||
|
||||
<body class="setup" ng-app="Application" ng-controller="RestoreController">
|
||||
|
||||
<a class="offline-banner animateMe" ng-show="client.offline" ng-cloak href="https://cloudron.io/documentation/troubleshooting/" target="_blank"><i class="fa fa-circle-notch fa-spin"></i> Cloudron is offline. Reconnecting...</a>
|
||||
<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">
|
||||
@@ -60,19 +73,87 @@
|
||||
<div class="col-md-10 col-md-offset-1 text-center">
|
||||
<h2>Cloudron Restore</h2>
|
||||
<p>Provide the backup to restore from</p>
|
||||
<br/>
|
||||
</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">
|
||||
<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="{{ config.webServerOrigin }}/documentation/backups/#storage-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<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 storageProvider" 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="username" 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="password" ng-disabled="busy" password-reveal>
|
||||
</div>
|
||||
|
||||
<!-- EXT4 -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.diskPath }" ng-show="provider === 'ext4'">
|
||||
<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'">
|
||||
</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>
|
||||
@@ -80,9 +161,9 @@
|
||||
</div>
|
||||
|
||||
<!-- S3/Minio/SOS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': error.endpoint }" ng-show="provider === 'minio' || provider === 's3-v4-compat'">
|
||||
<div class="form-group" ng-class="{ 'has-error': error.endpoint }" ng-show="provider === 'minio' || provider === 'upcloud-objectstorage' || provider === 'backblaze-b2' || provider === 's3-v4-compat'">
|
||||
<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 of Minio/S3 Compatible" ng-required="provider === 'minio' || provider === 's3-v4-compat'">
|
||||
<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 === 's3-v4-compat'">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="provider === 'minio' || provider === 's3-v4-compat'" >
|
||||
@@ -98,7 +179,7 @@
|
||||
<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="s3like(provider) || provider === 'gcs'">
|
||||
<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>
|
||||
@@ -108,6 +189,11 @@
|
||||
<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>
|
||||
@@ -133,6 +219,21 @@
|
||||
<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.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)">
|
||||
@@ -167,15 +268,25 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': error.key }" ng-show="provider !== 'noop'">
|
||||
<label class="control-label" for="inputConfigureBackupKey">Encryption key (optional)</label>
|
||||
<input type="text" class="form-control" ng-model="key" id="inputConfigureBackupKey" name="prefix" ng-disabled="busy" placeholder="Passphrase used to encrypt the backups">
|
||||
<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">
|
||||
<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="{{ config.webServerOrigin }}/documentation/networking/#ip-configuration" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<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>
|
||||
|
||||
@@ -216,7 +327,7 @@
|
||||
</div>
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted">©2020 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<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>
|
||||
|
||||
|
||||
+108
-60
@@ -4,25 +4,29 @@
|
||||
<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 Admin Setup </title>
|
||||
<title> Cloudron Setup </title>
|
||||
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css">
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.min.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>
|
||||
@@ -30,6 +34,15 @@
|
||||
<!-- 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/setup.js"></script>
|
||||
|
||||
@@ -37,70 +50,105 @@
|
||||
|
||||
<body class="setup" ng-app="Application" ng-controller="SetupController">
|
||||
|
||||
<a class="offline-banner animateMe" ng-show="client.offline" ng-cloak href="https://cloudron.io/documentation/troubleshooting/" target="_blank"><i class="fa fa-circle-notch fa-spin"></i> Cloudron is offline. Reconnecting...</a>
|
||||
<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>
|
||||
<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>
|
||||
</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 role="form" name="setupForm" ng-submit="activateCloudron()" novalidate>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<h1>Welcome to Cloudron</h1>
|
||||
<h3>Setup Admin Account</h3>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<div class="form-group" ng-class="{ 'has-error': setupForm.displayName.$dirty && setupForm.displayName.$invalid }">
|
||||
<label class="control-label">Full Name</label>
|
||||
<input type="text" class="form-control" ng-model="account.displayName" id="inputDisplayName" name="displayName" placeholder="Full Name" required autocomplete="off" autofocus>
|
||||
</div>
|
||||
<div ng-show="account.requireEmail" class="form-group" ng-class="{ 'has-error': setupForm.email.$dirty && setupForm.email.$invalid }">
|
||||
<label class="control-label">Email <sup><a ng-href="{{ webServerOrigin }}/documentation/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="account.email" id="inputEmail" name="email" placeholder="Email" required autocomplete="off" tooltip-class="long" tooltip-trigger="focus" uib-tooltip="This email address is local to your Cloudron and used for notifications and password reset. A valid email is also required for Let's Encrypt certificates.">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupForm.username.$dirty && setupForm.username.$invalid) || (!setupForm.username.$dirty && error.username) }">
|
||||
<label class="control-label">Username</label>
|
||||
<p ng-show="!setupForm.username.$dirty && error.username">{{ error.username }}</p>
|
||||
<input type="text" class="form-control" ng-model="account.username" id="inputUsername" name="username" placeholder="Username" ng-maxlength="512" ng-minlength="1" required autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': setupForm.password.$dirty && setupForm.password.$invalid }">
|
||||
<label class="control-label">Password</label>
|
||||
<input type="password" class="form-control" ng-model="account.password" id="inputPassword" name="password" placeholder="Password" ng-pattern="/^.{8,}$/" required autocomplete="off">
|
||||
<div class="control-label" ng-show="setupForm.password.$dirty && setupForm.password.$invalid">
|
||||
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be atleast 8 characters</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<input type="submit" class="btn btn-success" ng-disabled="setupForm.$invalid" value="Done">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<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 }">
|
||||
<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>
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted">©2020 <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>
|
||||
<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">
|
||||
<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>
|
||||
|
||||
+58
-42
@@ -10,33 +10,40 @@
|
||||
<link id="favicon" href="<%= apiOrigin %>/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css">
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css?<%= revision %>">
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.min.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>
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js"></script>
|
||||
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-1.6.4.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-target-blank.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js?<%= revision %>"></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-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>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/autofill-event.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>
|
||||
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js?<%= revision %>"></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/setupaccount.js"></script>
|
||||
<script type="text/javascript" src="/js/setupaccount.js?<%= revision %>"></script>
|
||||
|
||||
</head>
|
||||
|
||||
@@ -50,8 +57,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/>
|
||||
<h1><small>Welcome to</small> {{ status.cloudronName || 'Cloudron' }}</h1>
|
||||
<h3>Please setup your account</h3>
|
||||
<h1><small>{{ 'setupAccount.welcomeTo' | tr }}</small> {{ status.cloudronName || 'Cloudron' }}</h1>
|
||||
<h3>{{ 'setupAccount.description' | tr }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
@@ -65,57 +72,67 @@
|
||||
<form name="setupAccountForm" ng-submit="onSubmit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
|
||||
<div class="form-group" ng-show="existingUsername">
|
||||
<label class="control-label">Username</label>
|
||||
<input type="text" class="form-control" ng-model="username" name="username" readonly required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': ((setupAccountForm.username.$dirty && setupAccountForm.username.$invalid) || (!setupAccountForm.username.$dirty && error.username))}" ng-hide="existingUsername">
|
||||
<label class="control-label">Username</label>
|
||||
<div class="form-group" ng-class="{ 'has-error': ((setupAccountForm.username.$dirty && setupAccountForm.username.$invalid) || (!setupAccountForm.username.$dirty && error.username))}">
|
||||
<label class="control-label" for="inputUsername">{{ 'setupAccount.username' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="username" name="username" id="inputUsername" ng-disabled="profileLocked || existingUsername" required autofocus>
|
||||
<div class="control-label" ng-show="setupAccountForm.username.$dirty && setupAccountForm.username.$invalid">
|
||||
<small ng-show="setupAccountForm.username.$error.minlength">The username is too short</small>
|
||||
<small ng-show="setupAccountForm.username.$error.maxlength">The username is too long</small>
|
||||
<small ng-show="setupAccountForm.username.$dirty && setupAccountForm.username.$invalid">Not a valid username</small>
|
||||
<small ng-show="setupAccountForm.username.$error.minlength">{{ 'setupAccount.errorUsernameTooShort' | tr }}</small>
|
||||
<small ng-show="setupAccountForm.username.$error.maxlength">{{ 'setupAccount.errorUsernameTooLong' | tr }}</small>
|
||||
<small ng-show="setupAccountForm.username.$dirty && setupAccountForm.username.$invalid">{{ 'setupAccount.errorUsernameInvalid' | tr }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="username" name="username" id="inputUsername" required autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">Full Name</label>
|
||||
<input type="displayName" class="form-control" ng-model="displayName" name="displayName" required>
|
||||
<label class="control-label" for="inputDisplayName">{{ 'setupAccount.fullName' | tr }}</label>
|
||||
<input type="displayName" class="form-control" ng-model="displayName" name="displayName" id="inputDisplayName" ng-disabled="profileLocked" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupAccountForm.password.$dirty && setupAccountForm.password.$invalid) }">
|
||||
<label class="control-label">New Password</label>
|
||||
<label class="control-label" for="inputPassword">{{ 'setupAccount.password' | tr }}</label>
|
||||
<input type="password" class="form-control" ng-model="password" name="password" id="inputPassword" ng-pattern="/^.{8,}$/" required password-reveal>
|
||||
<div class="control-label" ng-show="setupAccountForm.password.$dirty && setupAccountForm.password.$invalid">
|
||||
<small ng-show="setupAccountForm.password.$dirty && setupAccountForm.password.$invalid">Password must be atleast 8 characters</small>
|
||||
<small ng-show="setupAccountForm.password.$dirty && setupAccountForm.password.$invalid">{{ 'setupAccount.errorPassword' | tr }}</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^.{8,}$/" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupAccountForm.passwordRepeat.$dirty && (password !== passwordRepeat)) }">
|
||||
<label class="control-label">Repeat Password</label>
|
||||
<label class="control-label" for="inputPasswordRepeat">{{ 'setupAccount.passwordRepeat' | tr }}</label>
|
||||
<input type="password" class="form-control" ng-model="passwordRepeat" name="passwordRepeat" id="inputPasswordRepeat" required password-reveal>
|
||||
<div class="control-label" ng-show="setupAccountForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
|
||||
<small ng-show="setupAccountForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
|
||||
<small ng-show="setupAccountForm.passwordRepeat.$dirty && (password !== passwordRepeat)">{{ 'setupAccount.errorPasswordNoMatch' | tr }}</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-outline pull-right" type="submit" ng-disabled="busy || setupAccountForm.$invalid || password !== passwordRepeat"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> Setup</button>
|
||||
<button class="btn btn-primary btn-outline pull-right" type="submit" ng-disabled="busy || setupAccountForm.$invalid || password !== passwordRepeat"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'setupAccount.setupAction' | tr }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout-content" ng-show="view === 'noUsername'">
|
||||
<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="<%= apiOrigin %>/api/v1/cloudron/avatar"/>
|
||||
<br/>
|
||||
<h2>{{ 'setupAccount.noUsername.title' | tr }}</h2>
|
||||
<br/>
|
||||
<p>{{ 'setupAccount.noUsername.description' | tr }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout-content" ng-show="view === 'invalidToken'">
|
||||
<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="<%= apiOrigin %>/api/v1/cloudron/avatar"/>
|
||||
<br/>
|
||||
<h2>Invalid or Expired Invite Link</h2>
|
||||
<h2>{{ 'setupAccount.invalidToken.title' | tr }}</h2>
|
||||
<br/>
|
||||
<p>Contact your server admin to get a new invite link.</p>
|
||||
<p>{{ 'setupAccount.invalidToken.description' | tr }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -127,9 +144,9 @@
|
||||
<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>Your Account is ready</h2>
|
||||
<h2>{{ 'setupAccount.success.title' | tr }}</h2>
|
||||
<br/>
|
||||
<a href="/" class="btn btn-primary">Open Dashboard</a>
|
||||
<a href="/" class="btn btn-primary">{{ 'setupAccount.success.openDashboardAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -137,7 +154,6 @@
|
||||
|
||||
<footer class="text-center ng-cloak">
|
||||
<span class="text-muted" ng-bind-html="status.footer | markdown2html"></span>
|
||||
<span class="version">v{{status.version}}</span>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
+83
-37
@@ -11,28 +11,42 @@
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css">
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.min.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>
|
||||
|
||||
@@ -40,14 +54,26 @@
|
||||
|
||||
<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">
|
||||
<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="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">
|
||||
@@ -55,23 +81,25 @@
|
||||
<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 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">
|
||||
<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">
|
||||
<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="{{ webServerOrigin }}/documentation/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>
|
||||
<p style="margin-top: 5px; font-size: 13px;">Apps will be installed on subdomains of this domain. You can add more domains later.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<h3 class="text-center">Domain Configuration <sup><a ng-href="{{ webServerOrigin }}/documentation/domains/#dns-providers" class="help" target="_blank" tabindex="-1"><i class="fa fa-question-circle"></i></a></sup> </h3>
|
||||
<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">
|
||||
@@ -123,6 +151,20 @@
|
||||
<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>
|
||||
@@ -163,14 +205,26 @@
|
||||
<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>
|
||||
|
||||
<!-- 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>Setup A records for <b>*.{{ dnsCredentials.domain || 'example.com' }}</b> and <b>{{ dnsCredentials.domain || 'example.com' }}</b> to this server's IP.</span>
|
||||
<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>Setup an A record for <b>my.{{ dnsCredentials.domain || 'example.com' }}</b> to this server's IP.<br/></span>
|
||||
<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>
|
||||
@@ -188,26 +242,18 @@
|
||||
|
||||
<br/>
|
||||
<div uib-collapse="!advancedVisible">
|
||||
<div ng-show="false">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="dnsCredentials.hyphenatedSubdomains" name="hyphenatedSubdomains" ng-disabled="dnsCredentials.busy"/> Hyphenate Subdomains
|
||||
</label>
|
||||
<p>When enabled, apps are installed into <code><location>-<domain></code></p>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">Zone Name (Optional) <sup><a ng-href="{{ webServerOrigin }}/documentation/domains/#zone-name" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<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="{{ webServerOrigin }}/documentation/certificates/#certificate-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<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="{{ webServerOrigin }}/documentation/networking/#ip-configuration" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<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>
|
||||
|
||||
@@ -241,7 +287,7 @@
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center"><small>Looking to <a href="/restore.html">restore?</a></small></div>
|
||||
<div class="col-md-12 text-center"><small>Looking to <a ng-href="/restore.html{{ search }}">restore?</a></small></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -250,7 +296,7 @@
|
||||
</div>
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted">©2020 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<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>
|
||||
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
<!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">
|
||||
|
||||
<!-- Fontawesome -->
|
||||
<link type="text/css" rel="stylesheet" href="<%= dashboardOrigin %>/3rdparty/fontawesome/css/all.css"/>
|
||||
|
||||
<style>
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
font-family: Roboto,Helvetica,Arial,sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.42857;
|
||||
color: #333;
|
||||
background-color: #e5e5e5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.layout-root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
*, ::after, ::before {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.layout-content {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.has-error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.row {
|
||||
margin-right: -15px;
|
||||
margin-left: -15px;
|
||||
}
|
||||
|
||||
.col-md-12 {
|
||||
width: 100%;
|
||||
float: left;
|
||||
position: relative;
|
||||
min-height: 1px;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
display: block;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.42857;
|
||||
color: #555;
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
padding: 6px 12px;
|
||||
background-color: #fff;
|
||||
background-image: none;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 2px;
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
|
||||
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #66afe9;
|
||||
outline: 0;
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 36px;
|
||||
font-weight: 500;
|
||||
line-height: 1.1;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 65%;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
margin-top: 100px;
|
||||
max-width: 620px;
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,.1);
|
||||
}
|
||||
|
||||
.btn.pull-right {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.pull-right {
|
||||
float: right !important;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #2196f3;
|
||||
border-color: #2196f3;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
margin-bottom: 0;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
touch-action: manipulation;
|
||||
cursor: pointer;
|
||||
background-image: none;
|
||||
border: 1px solid transparent;
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.42857;
|
||||
border-radius: 2px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
color: #fff;
|
||||
background-color: #0c7cd5;
|
||||
border-color: #0b76cc;
|
||||
}
|
||||
|
||||
.btn-primary.active.focus, .btn-primary.active:focus, .btn-primary.active:hover, .btn-primary:active.focus, .btn-primary:active:focus, .btn-primary:active:hover, .open > .btn-primary.dropdown-toggle.focus, .open > .btn-primary.dropdown-toggle:focus, .open > .btn-primary.dropdown-toggle:hover {
|
||||
color: #fff;
|
||||
background-color: #0a68b4;
|
||||
border-color: #08528d;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
</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>
|
||||
+142
-132
@@ -1,161 +1,171 @@
|
||||
<!DOCTYPE html>
|
||||
<html ng-app="Application" ng-controller="TerminalController">
|
||||
<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" />
|
||||
|
||||
<!-- this gets changed once we get the config (because angular has not loaded yet, we see template string for a flash) -->
|
||||
<title> Terminal </title>
|
||||
<title> Terminal </title>
|
||||
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
<link rel="apple-touch-icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar">
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
<link rel="apple-touch-icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar">
|
||||
|
||||
<!-- CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.css"/>
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/xterm/xterm.css">
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css">
|
||||
<!-- CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.css?<%= revision %>"/>
|
||||
<link type="text/css" rel="stylesheet" href="/theme.css?<%= revision %>">
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.min.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>
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/3rdparty/js/jquery.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js"></script>
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js?<%= revision %>"></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-animate.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-base64.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-sanitize.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script>
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Angularjs scripts -->
|
||||
<script type="text/javascript" src="/3rdparty/js/angular.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-animate.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-base64.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-sanitize.min.js?<%= revision %>"></script>
|
||||
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.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>
|
||||
<!-- 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>
|
||||
|
||||
<!-- Clipboard handling -->
|
||||
<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>
|
||||
|
||||
<!-- xterm -->
|
||||
<script type="text/javascript" src="/3rdparty/xterm/xterm.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/xterm/addons/attach/attach.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/xterm/addons/fit/fit.js"></script>
|
||||
<!-- Clipboard handling -->
|
||||
<script type="text/javascript" src="/3rdparty/js/clipboard.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Main Application -->
|
||||
<script type="text/javascript" src="/js/terminal.js"></script>
|
||||
<!-- xterm -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/xterm/css/xterm.css?<%= revision %>" />
|
||||
<script src="/3rdparty/xterm/lib/xterm.js?<%= revision %>"></script>
|
||||
<script src="/3rdparty/xterm-addon-attach/lib/xterm-addon-attach.js?<%= revision %>"></script>
|
||||
<script src="/3rdparty/xterm-addon-fit/lib/xterm-addon-fit.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></script>
|
||||
|
||||
<!-- Main Application -->
|
||||
<script type="text/javascript" src="/js/terminal.js?<%= revision %>"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body style="overflow: hidden;">
|
||||
|
||||
<!-- Modal download file -->
|
||||
<div class="modal fade" id="downloadFileModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Download from {{ selected.name }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="downloadFileForm" ng-submit="downloadFile.submit()">
|
||||
<div class="form-group" ng-class="{ 'has-error': downloadFileForm.filePath.$dirty && downloadFile.error }">
|
||||
<label class="control-label" for="inputDownloadFilePath">Path to file or directory</label>
|
||||
<div class="control-label" ng-show="{ 'has-error': downloadFileForm.filePath.$dirty && downloadFile.error }">
|
||||
<small>{{ downloadFile.error }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" name="filePath" ng-model="downloadFile.filePath" required autofocus>
|
||||
</div>
|
||||
<input id="inputDownloadFilePath" class="ng-hide" type="submit" ng-disabled="!downloadFile.filePath"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a id="fileDownloadLink" class="" ng-href="{{ downloadFile.downloadUrl() }}" target="_blank"></a>
|
||||
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="downloadFile.submit()" ng-disabled="!downloadFile.filePath"><i class="fa fa-circle-notch fa-spin" ng-show="downloadFile.busy"></i> Download</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Modal download file -->
|
||||
<div class="modal fade" id="downloadFileModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'terminal.download.title' | tr:{ name: selected.name } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="downloadFileForm" ng-submit="downloadFile.submit()">
|
||||
<div class="form-group" ng-class="{ 'has-error': downloadFileForm.filePath.$dirty && downloadFile.error }">
|
||||
<label class="control-label" for="inputDownloadFilePath">{{ 'terminal.download.filePath' | tr }}</label>
|
||||
<div class="control-label" ng-show="{ 'has-error': downloadFileForm.filePath.$dirty && downloadFile.error }">
|
||||
<small>{{ downloadFile.error }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" name="filePath" ng-model="downloadFile.filePath" required autofocus>
|
||||
</div>
|
||||
<input id="inputDownloadFilePath" class="ng-hide" type="submit" ng-disabled="!downloadFile.filePath"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a id="fileDownloadLink" class="" ng-href="{{ downloadFile.downloadUrl() }}" target="_blank"></a>
|
||||
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="downloadFile.submit()" ng-disabled="!downloadFile.filePath"><i class="fa fa-circle-notch fa-spin" ng-show="downloadFile.busy"></i> {{ 'terminal.download.download' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal upload progress -->
|
||||
<div class="modal fade" id="uploadProgressModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'terminal.upload.title' | tr:{ name: selected.name } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<span><b>{{ uploadProgress.current | prettyByteSize }}</b> (total {{ uploadProgress.total | prettyByteSize }})</span>
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ 100*(uploadProgress.current/uploadProgress.total) }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="animateMe ng-hide layout-root terminal-view" ng-show="initialized">
|
||||
|
||||
<div class="terminal-controls">
|
||||
<h3 style="display: inline-block;">{{ selected.name }}</h3>
|
||||
|
||||
<input type="file" id="fileUpload" class="hide"/>
|
||||
|
||||
<div class="pull-right">
|
||||
<div class="btn-group" ng-show="usesAddon('scheduler')">
|
||||
<button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" ng-disabled="appBusy">
|
||||
{{ 'terminal.scheduler' | tr }} <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in schedulerTasks"><a href="" ng-click="terminalInject('scheduler', task)">{{ task.name }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- addon actions -->
|
||||
<button class="btn btn-success" ng-click="terminalInject('mysql')" ng-show="usesAddon('mysql')" ng-disabled="appBusy">MySQL</button>
|
||||
<button class="btn btn-success" ng-click="terminalInject('postgresql')" ng-show="usesAddon('postgresql')" ng-disabled="appBusy">Postgres</button>
|
||||
<button class="btn btn-success" ng-click="terminalInject('mongodb')" ng-show="usesAddon('mongodb')" ng-disabled="appBusy">MongoDB</button>
|
||||
<button class="btn btn-success" ng-click="terminalInject('redis')" ng-show="usesAddon('redis')" ng-disabled="appBusy">Redis</button>
|
||||
|
||||
<!-- terminal actions -->
|
||||
<button class="btn btn-primary" ng-click="restartApp()" ng-show="selected.type === 'app'" ng-disabled="appBusy"><i class="fa fa-sync-alt" ng-class="{ 'fa-spin': restartAppBusy }"></i> {{ 'terminal.restart' | tr }}</button>
|
||||
<button class="btn btn-primary" ng-click="uploadFile()" ng-show="selected.type === 'app' && !uploadProgress.busy" ng-disabled="appBusy"><i class="fa fa-upload"></i> {{ 'terminal.uploadToTmp' | tr }}</button>
|
||||
<button class="btn btn-primary" ng-click="uploadProgress.show()" ng-show="uploadProgress.busy" ng-disabled="appBusy"><i class="fa fa-circle-notch fa-spin"></i> {{ 'terminal.uploading' | tr }}</button>
|
||||
<button class="btn btn-primary" ng-click="downloadFile.show()" ng-show="selected.type === 'app'" ng-disabled="appBusy"><i class="fa fa-download"></i> {{ 'terminal.downloadAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal upload progress -->
|
||||
<div class="modal fade" id="uploadProgressModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Uploading file to {{ selected.name }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<span><b>{{ (uploadProgress.current/1000/1000).toFixed(2) }}MB</b> (total {{ (uploadProgress.total/1000/1000).toFixed(2) }}MB)</span>
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ 100*(uploadProgress.current/uploadProgress.total) }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="terminal-container" id="terminalContainer" ng-hide="appBusy"></div>
|
||||
<div class="terminal-container placeholder" ng-show="appBusy">
|
||||
<h4>
|
||||
<span ng-show="restartAppBusy">{{ 'terminal.busy.restarting' | tr }}</span>
|
||||
<span ng-show="selectedAppInfo.installationState === 'pending_debug' && selectedAppInfo.debugMode">{{ 'terminal.busy.restartingInPausedMode' | tr }}</span>
|
||||
<span ng-show="selectedAppInfo.installationState === 'pending_debug' && !selectedAppInfo.debugMode ">{{ 'terminal.busy.resuming' | tr }}</span>
|
||||
<span ng-show="selectedAppInfo.installationState === 'pending_installed'">{{ 'terminal.busy.installing' | tr }}</span>
|
||||
</h4>
|
||||
|
||||
<div class="progress" ng-show="appBusy" style="width: 80%">
|
||||
<div class="progress-bar progress-bar-striped active" role="progressbar" style="width: 100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="animateMe ng-hide layout-root terminal-view" ng-show="initialized">
|
||||
|
||||
<div class="terminal-controls">
|
||||
<h3 style="display: inline-block;">{{ selected.name }}</h3>
|
||||
|
||||
<input type="file" id="fileUpload" class="hide"/>
|
||||
|
||||
<div class="pull-right">
|
||||
<div class="btn-group" ng-show="usesAddon('scheduler')">
|
||||
<button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" ng-disabled="appBusy">
|
||||
Scheduler/Cron <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in schedulerTasks"><a href="" ng-click="terminalInject('scheduler', task)">{{ task.name }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- addon actions -->
|
||||
<button class="btn btn-success" ng-click="terminalInject('mysql')" ng-show="usesAddon('mysql')" ng-disabled="appBusy">MySQL</button>
|
||||
<button class="btn btn-success" ng-click="terminalInject('postgresql')" ng-show="usesAddon('postgresql')" ng-disabled="appBusy">Postgres</button>
|
||||
<button class="btn btn-success" ng-click="terminalInject('mongodb')" ng-show="usesAddon('mongodb')" ng-disabled="appBusy">MongoDB</button>
|
||||
<button class="btn btn-success" ng-click="terminalInject('redis')" ng-show="usesAddon('redis')" ng-disabled="appBusy">Redis</button>
|
||||
|
||||
<!-- terminal actions -->
|
||||
<button class="btn btn-primary" ng-click="restartApp()" ng-show="selected.type === 'app'" ng-disabled="appBusy"><i class="fa fa-sync-alt" ng-class="{ 'fa-spin': restartAppBusy }"></i> Restart</button>
|
||||
<button class="btn btn-primary" ng-click="uploadFile()" ng-show="selected.type === 'app' && !uploadProgress.busy" ng-disabled="appBusy"><i class="fa fa-upload"></i> Upload to /tmp</button>
|
||||
<button class="btn btn-primary" ng-click="uploadProgress.show()" ng-show="uploadProgress.busy" ng-disabled="appBusy"><i class="fa fa-circle-notch fa-spin"></i> Uploading...</button>
|
||||
<button class="btn btn-primary" ng-click="downloadFile.show()" ng-show="selected.type === 'app'" ng-disabled="appBusy"><i class="fa fa-download"></i> Download</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="terminal-container" id="terminalContainer" ng-hide="appBusy"></div>
|
||||
<div class="terminal-container placeholder" ng-show="appBusy">
|
||||
<h4>
|
||||
<span ng-show="restartAppBusy">Restarting app...</span>
|
||||
<span ng-show="selectedAppInfo.installationState === 'pending_debug' && selectedAppInfo.debugMode">Restarting app in paused mode...</span>
|
||||
<span ng-show="selectedAppInfo.installationState === 'pending_debug' && !selectedAppInfo.debugMode ">App is being resumed...</span>
|
||||
<span ng-show="selectedAppInfo.installationState === 'pending_installed'">App is being installed...</span>
|
||||
</h4>
|
||||
|
||||
<div class="progress" ng-show="appBusy" style="width: 80%">
|
||||
<div class="progress-bar progress-bar-striped active" role="progressbar" style="width: 100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contextMenuBackdrop">
|
||||
<ul class="dropdown-menu" id="terminalContextMenu" style="position: absolute; display:none;">
|
||||
<li><a href="" ng-click="terminalCopy()">Copy</a></li>
|
||||
<li class="disabled"><a>For Paste use Ctrl+v</a></li>
|
||||
<li role="separator" class="divider"></li>
|
||||
<li><a href="" ng-click="terminalClear()">Clear</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="contextMenuBackdrop">
|
||||
<ul class="dropdown-menu" id="terminalContextMenu" style="position: absolute; display:none;">
|
||||
<li><a href="" ng-click="terminalCopy()">{{ 'terminal.contextmenu.copy' | tr }}</a></li>
|
||||
<li class="disabled"><a>{{ 'terminal.contextmenu.pasteInfo' | tr }}</a></li>
|
||||
<li role="separator" class="divider"></li>
|
||||
<li><a href="" ng-click="terminalClear()">{{ 'terminal.contextmenu.clear' | tr }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+731
-128
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,419 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular:false */
|
||||
/* global $:false */
|
||||
|
||||
angular.module('Application').controller('ActivityController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
$scope.busy = false;
|
||||
$scope.eventLogs = [];
|
||||
$scope.activeEventLog = null;
|
||||
|
||||
// TODO sync this with the eventlog filter
|
||||
$scope.actions = [
|
||||
{ name: '-- All app events --', value: 'app.' },
|
||||
{ name: '-- All user events --', value: 'user.' },
|
||||
{ name: 'app.configure', value: 'app.configure' },
|
||||
{ name: 'app.install', value: 'app.install' },
|
||||
{ name: 'app.restore', value: 'app.restore' },
|
||||
{ name: 'app.uninstall', value: 'app.uninstall' },
|
||||
{ name: 'app.update', value: 'app.update' },
|
||||
{ name: 'app.update.finish', value: 'app.update.finish' },
|
||||
{ name: 'app.login', value: 'app.login' },
|
||||
{ name: 'app.oom', value: 'app.oom' },
|
||||
{ name: 'app.down', value: 'app.down' },
|
||||
{ name: 'app.up', value: 'app.up' },
|
||||
{ name: 'Apptask Crash', value: 'app.task.crash' },
|
||||
{ name: 'backup.cleanup', value: 'backup.cleanup.start' },
|
||||
{ name: 'backup.cleanup.finish', value: 'backup.cleanup.finish' },
|
||||
{ name: 'backup.finish', value: 'backup.finish' },
|
||||
{ name: 'backup.start', value: 'backup.start' },
|
||||
{ name: 'certificate.new', value: 'certificate.new' },
|
||||
{ name: 'certificate.renew', value: 'certificate.renew' },
|
||||
{ name: 'cloudron.activate', value: 'cloudron.activate' },
|
||||
{ name: 'cloudron.provision', value: 'cloudron.provision' },
|
||||
{ name: 'cloudron.restore', value: 'cloudron.restore' },
|
||||
{ name: 'cloudron.start', value: 'cloudron.start' },
|
||||
{ name: 'cloudron.update', value: 'cloudron.update' },
|
||||
{ name: 'cloudron.update.finish', value: 'cloudron.update.finish' },
|
||||
{ name: 'dashboard.domain.update', value: 'dashboard.domain.update' },
|
||||
{ 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: 'mail.enabled', value: 'mail.enabled' },
|
||||
{ name: 'mail.box.add', value: 'mail.box.add' },
|
||||
{ name: 'mail.box.update', value: 'mail.box.update' },
|
||||
{ name: 'mail.box.remove', value: 'mail.box.remove' },
|
||||
{ name: 'mail.list.add', value: 'mail.list.add' },
|
||||
{ name: 'mail.list.update', value: 'mail.list.update' },
|
||||
{ name: 'mail.list.remove', value: 'mail.list.remove' },
|
||||
{ name: 'support.ticket', value: 'support.ticket' },
|
||||
{ name: 'support.ssh', value: 'support.ssh' },
|
||||
{ name: 'user.add', value: 'user.add' },
|
||||
{ name: 'user.login', value: 'user.login' },
|
||||
{ name: 'user.remove', value: 'user.remove' },
|
||||
{ name: 'user.transfer', value: 'user.transfer' },
|
||||
{ name: 'user.update', value: 'user.update' },
|
||||
{ name: 'System Crash', value: 'system.crash' }
|
||||
];
|
||||
|
||||
$scope.pageItemCount = [
|
||||
{ name: 'Show 20 per page', value: 20 },
|
||||
{ name: 'Show 50 per page', value: 50 },
|
||||
{ name: 'Show 100 per page', value: 100 }
|
||||
];
|
||||
|
||||
$scope.currentPage = 1;
|
||||
$scope.pageItems = $scope.pageItemCount[0];
|
||||
$scope.action = '';
|
||||
$scope.selectedActions = [];
|
||||
$scope.search = '';
|
||||
|
||||
function eventLogDetails(eventLog) {
|
||||
var ACTION_ACTIVATE = 'cloudron.activate';
|
||||
var ACTION_PROVISION = 'cloudron.provision';
|
||||
var ACTION_RESTORE = 'cloudron.restore';
|
||||
|
||||
var ACTION_APP_CLONE = 'app.clone';
|
||||
var ACTION_APP_REPAIR = 'app.repair';
|
||||
var ACTION_APP_CONFIGURE = 'app.configure';
|
||||
var ACTION_APP_INSTALL = 'app.install';
|
||||
var ACTION_APP_RESTORE = 'app.restore';
|
||||
var ACTION_APP_UNINSTALL = 'app.uninstall';
|
||||
var ACTION_APP_UPDATE = 'app.update';
|
||||
var ACTION_APP_UPDATE_FINISH = 'app.update.finish';
|
||||
var ACTION_APP_LOGIN = 'app.login';
|
||||
var ACTION_APP_OOM = 'app.oom';
|
||||
var ACTION_APP_UP = 'app.up';
|
||||
var ACTION_APP_DOWN = 'app.down';
|
||||
|
||||
var ACTION_BACKUP_FINISH = 'backup.finish';
|
||||
var ACTION_BACKUP_START = 'backup.start';
|
||||
var ACTION_BACKUP_CLEANUP_START = 'backup.cleanup.start';
|
||||
var ACTION_BACKUP_CLEANUP_FINISH = 'backup.cleanup.finish';
|
||||
var ACTION_CERTIFICATE_NEW = 'certificate.new';
|
||||
var ACTION_CERTIFICATE_RENEWAL = 'certificate.renew';
|
||||
|
||||
var ACTION_DASHBOARD_DOMAIN_UPDATE = 'dashboard.domain.update';
|
||||
|
||||
var ACTION_DOMAIN_ADD = 'domain.add';
|
||||
var ACTION_DOMAIN_UPDATE = 'domain.update';
|
||||
var ACTION_DOMAIN_REMOVE = 'domain.remove';
|
||||
|
||||
var ACTION_START = 'cloudron.start';
|
||||
var ACTION_UPDATE = 'cloudron.update';
|
||||
var ACTION_UPDATE_FINISH = 'cloudron.update.finish';
|
||||
var ACTION_USER_ADD = 'user.add';
|
||||
var ACTION_USER_LOGIN = 'user.login';
|
||||
var ACTION_USER_REMOVE = 'user.remove';
|
||||
var ACTION_USER_UPDATE = 'user.update';
|
||||
var ACTION_USER_TRANSFER = 'user.transfer';
|
||||
|
||||
var ACTION_MAIL_ENABLED = 'mail.enabled';
|
||||
var ACTION_MAIL_DISABLED = 'mail.disabled';
|
||||
var ACTION_MAIL_MAILBOX_ADD = 'mail.box.add';
|
||||
var ACTION_MAIL_MAILBOX_UPDATE = 'mail.box.update';
|
||||
var ACTION_MAIL_MAILBOX_REMOVE = 'mail.box.remove';
|
||||
var ACTION_MAIL_LIST_ADD = 'mail.list.add';
|
||||
var ACTION_MAIL_LIST_UPDATE = 'mail.list.update';
|
||||
var ACTION_MAIL_LIST_REMOVE = 'mail.list.remove';
|
||||
|
||||
var ACTION_SUPPORT_TICKET = 'support.ticket';
|
||||
var ACTION_SUPPORT_SSH = 'support.ssh';
|
||||
|
||||
var ACTION_DYNDNS_UPDATE = 'dyndns.update';
|
||||
|
||||
var ACTION_SYSTEM_CRASH = 'system.crash';
|
||||
|
||||
var data = eventLog.data;
|
||||
var errorMessage = data.errorMessage;
|
||||
var details;
|
||||
|
||||
switch (eventLog.action) {
|
||||
case ACTION_ACTIVATE:
|
||||
return 'Cloudron was activated';
|
||||
|
||||
case ACTION_PROVISION:
|
||||
return 'Cloudron was setup';
|
||||
|
||||
case ACTION_RESTORE:
|
||||
return 'Cloudron was restored using backup ' + data.backupId;
|
||||
|
||||
case ACTION_APP_CONFIGURE:
|
||||
if (!data.app) return '';
|
||||
|
||||
var q = function (x) {
|
||||
return '"' + x + '"';
|
||||
};
|
||||
var name = (data.app.label || data.app.fqdn || data.app.location) + ' (' + data.app.manifest.title + ')';
|
||||
|
||||
if ('accessRestriction' in data) { // since it can be null
|
||||
return 'Access restriction of ' + name + ' was changed';
|
||||
} else if (data.label) {
|
||||
return 'Label of ' + name + ' was set to ' + q(data.label);
|
||||
} else if (data.tags) {
|
||||
return 'Tags of ' + name + ' was set to ' + q(data.tags.join(','));
|
||||
} else if (data.icon) {
|
||||
return 'Icon of ' + name + ' was changed';
|
||||
} else if (data.memoryLimit) {
|
||||
return 'Memory limit of ' + name + ' was set to ' + data.memoryLimit;
|
||||
} else if (data.env) {
|
||||
return 'Env vars of ' + name + ' was changed';
|
||||
} else if ('debugMode' in data) { // since it can be null
|
||||
if (data.debugMode) {
|
||||
return name + ' was placed in repair mode';
|
||||
} else {
|
||||
return name + ' was taken out of repair mode';
|
||||
}
|
||||
} else if (('mailboxName' in data) && data.mailboxName !== data.app.mailboxName) {
|
||||
if (data.mailboxName) {
|
||||
return 'Mailbox of ' + name + ' was set to ' + q(data.mailboxName);
|
||||
} else {
|
||||
return 'Mailbox of ' + name + ' was reset';
|
||||
}
|
||||
} else if ('enableBackup' in data) {
|
||||
return 'Automatic backups of ' + name + ' was ' + (data.enableBackup ? 'enabled' : 'disabled');
|
||||
} else if ('enableAutomaticUpdate' in data) {
|
||||
return 'Automatic updates of ' + name + ' was ' + (data.enableAutomaticUpdate ? 'enabled' : 'disabled');
|
||||
} else if ('reverseProxyConfig' in data) {
|
||||
return 'Reverse proxy configuration of ' + name + ' was updated';
|
||||
} else if ('cert' in data) {
|
||||
if (data.cert) {
|
||||
return 'Custom certificate was set for ' + name;
|
||||
} else {
|
||||
return 'Certificate of ' + name + ' was reset';
|
||||
}
|
||||
} else if (data.location) {
|
||||
if (data.fqdn !== data.app.fqdn) {
|
||||
return 'Location of ' + name + ' was changed to ' + data.fqdn;
|
||||
} else if (!angular.equals(data.alternateDomains, data.app.alternateDomains)) {
|
||||
var altFqdns = data.alternateDomains.map(function (a) { return a.fqdn; });
|
||||
return 'Alternate domains of ' + name + ' was ' + (altFqdns.length ? 'set to ' + altFqdns.join(', ') : 'reset');
|
||||
} else if (!angular.equals(data.portBindings, data.app.portBindings)) {
|
||||
return 'Port bindings of ' + name + ' was changed';
|
||||
}
|
||||
} else if ('dataDir' in data) {
|
||||
if (data.dataDir) {
|
||||
return 'Data directory of ' + name + ' was set ' + data.dataDir;
|
||||
} else {
|
||||
return 'Data directory of ' + name + ' was reset';
|
||||
}
|
||||
} else if ('icon' in data) {
|
||||
if (data.icon) {
|
||||
return 'Icon of ' + name + ' was set';
|
||||
} else {
|
||||
return 'Icon of ' + name + ' was reset';
|
||||
}
|
||||
}
|
||||
|
||||
return data.app.manifest.title + ' was re-configured at ' + (data.app.fqdn || data.app.location);
|
||||
|
||||
case ACTION_APP_INSTALL:
|
||||
if (!data.app) return '';
|
||||
return data.app.manifest.title + ' (package v' + data.app.manifest.version + ') was installed at ' + (data.app.fqdn || data.app.location);
|
||||
|
||||
case ACTION_APP_RESTORE:
|
||||
if (!data.app) return '';
|
||||
details = data.app.manifest.title + ' was restored at ' + (data.app.fqdn || data.app.location);
|
||||
// older versions (<3.5) did not have these fields
|
||||
if (data.fromManifest) details += ' from version ' + data.fromManifest.version;
|
||||
if (data.toManifest) details += ' to version ' + data.toManifest.version;
|
||||
if (data.backupId) details += ' using backup ' + data.backupId;
|
||||
return details;
|
||||
|
||||
case ACTION_APP_UNINSTALL:
|
||||
if (!data.app) return '';
|
||||
return data.app.manifest.title + ' (package v' + data.app.manifest.version + ') was uninstalled at ' + (data.app.fqdn || data.app.location);
|
||||
|
||||
case ACTION_APP_UPDATE:
|
||||
if (!data.app) return '';
|
||||
return 'Update of ' + data.app.manifest.title + ' at ' + (data.app.fqdn || data.app.location) + ' started from v' + data.fromManifest.version + ' to v' + data.toManifest.version;
|
||||
|
||||
case ACTION_APP_UPDATE_FINISH:
|
||||
if (!data.app) return '';
|
||||
return data.app.manifest.title + ' at ' + (data.app.fqdn || data.app.location) + ' was updated to v' + data.app.manifest.version;
|
||||
|
||||
case ACTION_APP_CLONE:
|
||||
return data.newApp.manifest.title + ' at ' + (data.newApp.fqdn || data.newApp.location) + ' was cloned from ' + (data.oldApp.fqdn || data.oldApp.location) + ' using backup ' + data.backupId + ' with v' + data.oldApp.manifest.version;
|
||||
|
||||
case ACTION_APP_REPAIR:
|
||||
return 'App at ' + data.app.fqdn + ' was repaired';
|
||||
|
||||
case ACTION_APP_LOGIN:
|
||||
var app = Client.getCachedAppSync(data.appId);
|
||||
if (!app) return '';
|
||||
return 'App ' + app.fqdn + ' logged in';
|
||||
|
||||
case ACTION_APP_OOM:
|
||||
if (!data.app) return '';
|
||||
return data.app.manifest.title + ' ran out of memory';
|
||||
|
||||
case ACTION_APP_DOWN:
|
||||
if (!data.app) return '';
|
||||
return data.app.manifest.title + ' is down';
|
||||
|
||||
case ACTION_APP_UP:
|
||||
if (!data.app) return '';
|
||||
return data.app.manifest.title + ' is back online';
|
||||
|
||||
case ACTION_BACKUP_START:
|
||||
return 'Backup started';
|
||||
|
||||
case ACTION_BACKUP_FINISH:
|
||||
if (!errorMessage) {
|
||||
return 'Cloudron backup created with Id ' + data.backupId;
|
||||
} else {
|
||||
return 'Cloudron backup errored with error: ' + errorMessage;
|
||||
}
|
||||
|
||||
case ACTION_BACKUP_CLEANUP_START:
|
||||
return 'Backup cleaner started';
|
||||
|
||||
case ACTION_BACKUP_CLEANUP_FINISH:
|
||||
return data.errorMessage ? 'Backup cleaner errored: ' + data.errorMessage : 'Backup cleaner removed ' + data.removedBoxBackups.length + ' backups';
|
||||
|
||||
case ACTION_CERTIFICATE_NEW:
|
||||
return 'Certificate install for ' + data.domain + (errorMessage ? ' failed' : ' succeeded');
|
||||
|
||||
case ACTION_CERTIFICATE_RENEWAL:
|
||||
return 'Certificate renewal for ' + data.domain + (errorMessage ? ' failed' : ' succeeded');
|
||||
|
||||
case ACTION_DASHBOARD_DOMAIN_UPDATE:
|
||||
return 'Dashboard domain set to ' + data.fqdn;
|
||||
|
||||
case ACTION_DOMAIN_ADD:
|
||||
return 'Domain ' + data.domain + ' with ' + data.provider + ' provider was added';
|
||||
|
||||
case ACTION_DOMAIN_UPDATE:
|
||||
return 'Domain ' + data.domain + ' with ' + data.provider + ' provider was updated';
|
||||
|
||||
case ACTION_DOMAIN_REMOVE:
|
||||
return 'Domain ' + data.domain + ' was removed';
|
||||
|
||||
case ACTION_MAIL_ENABLED:
|
||||
return 'Cloudron Mail was enabled for domain ' + data.domain;
|
||||
|
||||
case ACTION_MAIL_DISABLED:
|
||||
return 'Cloudron Mail was disabled for domain ' + data.domain;
|
||||
|
||||
case ACTION_MAIL_MAILBOX_ADD:
|
||||
return 'Mailbox with name ' + data.name + ' was added in domain ' + data.domain;
|
||||
|
||||
case ACTION_MAIL_MAILBOX_UPDATE:
|
||||
return 'Mailbox with name ' + data.name + ' was updated in domain ' + data.domain;
|
||||
|
||||
case ACTION_MAIL_MAILBOX_REMOVE:
|
||||
return 'Mailbox with name ' + data.name + ' was removed in domain ' + data.domain;
|
||||
|
||||
case ACTION_MAIL_LIST_ADD:
|
||||
return 'Mail list with name ' + data.name + ' was added in domain ' + data.domain;
|
||||
|
||||
case ACTION_MAIL_LIST_UPDATE:
|
||||
return 'Mail list with name ' + data.name + ' was updated in domain ' + data.domain;
|
||||
|
||||
case ACTION_MAIL_LIST_REMOVE:
|
||||
return 'Mail list with name ' + data.name + ' was removed in domain ' + data.domain;
|
||||
|
||||
case ACTION_START:
|
||||
return 'Cloudron started with version ' + data.version;
|
||||
|
||||
case ACTION_UPDATE:
|
||||
return 'Cloudron update to version ' + data.boxUpdateInfo.version + ' was started';
|
||||
|
||||
case ACTION_UPDATE_FINISH:
|
||||
return 'Cloudron updated to version ' + data.newVersion;
|
||||
|
||||
case ACTION_USER_ADD:
|
||||
return data.email + (data.user.username ? ' (' + data.user.username + ')' : '') + ' was added';
|
||||
|
||||
case ACTION_USER_UPDATE:
|
||||
return (data.user ? (data.user.email + (data.user.username ? ' (' + data.user.username + ')' : '')) : data.userId) + ' was updated';
|
||||
|
||||
case ACTION_USER_REMOVE:
|
||||
return (data.user ? (data.user.email + (data.user.username ? ' (' + data.user.username + ')' : '')) : data.userId) + ' was removed';
|
||||
|
||||
case ACTION_USER_TRANSFER:
|
||||
return 'Apps of ' + data.oldOwnerId + ' was transferred to ' + data.newOwnerId;
|
||||
|
||||
case ACTION_USER_LOGIN:
|
||||
return (data.user ? data.user.username : data.userId) + ' logged in';
|
||||
|
||||
case ACTION_DYNDNS_UPDATE:
|
||||
return 'DNS was updated from ' + data.fromIp + ' to ' + data.toIp;
|
||||
|
||||
case ACTION_SUPPORT_SSH:
|
||||
return 'Remote Support was ' + (data.enable ? 'enabled' : 'disabled');
|
||||
|
||||
case ACTION_SUPPORT_TICKET:
|
||||
return 'Support ticket was created';
|
||||
|
||||
case ACTION_SYSTEM_CRASH:
|
||||
return 'A system process crashed';
|
||||
|
||||
default: return eventLog.action;
|
||||
}
|
||||
}
|
||||
|
||||
function eventLogSource(eventLog) {
|
||||
var source = eventLog.source;
|
||||
var line = '';
|
||||
|
||||
line = source.username || source.userId || source.mailboxId || source.authType || 'system';
|
||||
if (source.appId) {
|
||||
var app = Client.getCachedAppSync(source.appId);
|
||||
line += ' - ' + (app ? app.fqdn : source.appId);
|
||||
} else if (source.ip) {
|
||||
line += ' - ' + source.ip;
|
||||
}
|
||||
|
||||
return line;
|
||||
}
|
||||
|
||||
function fetchEventLogs() {
|
||||
$scope.busy = true;
|
||||
var actions = $scope.selectedActions.map(function (a) { return a.value; }).join(', ');
|
||||
|
||||
Client.getEventLogs(actions, $scope.search || null, $scope.currentPage, $scope.pageItems.value, function (error, result) {
|
||||
$scope.busy = false;
|
||||
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.eventLogs = [];
|
||||
result.forEach(function (e) {
|
||||
$scope.eventLogs.push({ raw: e, details: eventLogDetails(e), source: eventLogSource(e) });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$scope.showNextPage = function () {
|
||||
$scope.currentPage++;
|
||||
fetchEventLogs();
|
||||
};
|
||||
|
||||
$scope.showPrevPage = function () {
|
||||
if ($scope.currentPage > 1) $scope.currentPage--;
|
||||
else $scope.currentPage = 1;
|
||||
|
||||
fetchEventLogs();
|
||||
};
|
||||
|
||||
$scope.updateFilter = function (fresh) {
|
||||
if (fresh) $scope.currentPage = 1;
|
||||
fetchEventLogs();
|
||||
};
|
||||
|
||||
$scope.showEventLogDetails = function (eventLog) {
|
||||
if ($scope.activeEventLog === eventLog) $scope.activeEventLog = null;
|
||||
else $scope.activeEventLog = eventLog;
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
fetchEventLogs();
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
+976
-558
File diff suppressed because it is too large
Load Diff
+845
-234
File diff suppressed because it is too large
Load Diff
+47
-49
@@ -6,69 +6,63 @@
|
||||
<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">Package <a ng-href="/#/appstore/{{appPostInstallConfirm.app.manifest.id}}?version={{appPostInstallConfirm.app.manifest.version}}">v{{ appPostInstallConfirm.app.manifest.version }}</a> </span>
|
||||
<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}}">Documentation</a> </span>
|
||||
<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-bind-html="appPostInstallConfirm.app.manifest.postInstallMessage | postInstallMessage:appPostInstallConfirm.app | markdown2html"></div>
|
||||
<div ng-show="appPostInstallConfirm.app.manifest.documentationUrl">
|
||||
Please see the <a target="_blank" ng-href="{{appPostInstallConfirm.app.manifest.documentationUrl}}">documentation</a> for more information.
|
||||
</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">Acknowledge instructions</label>
|
||||
<label class="control-label" for="appPostInstallConfirmCheckbox">{{ 'app.appInfo.postInstallConfirmCheckbox' | tr }}</label>
|
||||
</div>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<a class="btn btn-success" ng-href="{{ appPostInstallConfirm.confirmed ? ('https://' + appPostInstallConfirm.app.fqdn) : '' }}" target="_blank" ng-disabled="!appPostInstallConfirm.confirmed" ng-click="appPostInstallConfirm.submit()">Open {{ appPostInstallConfirm.app.manifest.title }}</a>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function imageErrorHandler(elem) {
|
||||
'use strict';
|
||||
|
||||
elem.src = elem.getAttribute('fallback-icon');
|
||||
elem.onerror = null; // avoid retry after default icon cannot be loaded
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="content content-large">
|
||||
|
||||
<!-- Workaround for select-all issue, see commit message -->
|
||||
<div style="font-size: 1px;"> </div>
|
||||
|
||||
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length === 0 && user.isAtLeastAdmin">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<br/><br/><br/><br/>
|
||||
<h1><i class="fa fa-cloud-download fa-fw"></i> No apps installed yet!</h1>
|
||||
<br/></br>
|
||||
<h3>How about installing some? Check out the <a href="#/appstore">App Store</a></h3>
|
||||
</div>
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<br/><br/><br/><br/>
|
||||
<h1><i class="fa fa-cloud-download fa-fw"></i> {{ 'apps.noApps.title' | tr }}</h1>
|
||||
<br/></br>
|
||||
<h3 ng-bind-html="'apps.noApps.description' | tr:{ appStoreLink: '#/appstore' }"></h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length === 0 && !user.isAtLeastAdmin">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<br/><br/><br/><br/>
|
||||
<h1>You don't have access to any apps yet!</h1>
|
||||
<br/></br>
|
||||
<h3>Once you do, they will show up here.</h3>
|
||||
</div>
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<br/><br/><br/><br/>
|
||||
<h1>{{ 'apps.noAccess.title' | tr }}</h1>
|
||||
<br/></br>
|
||||
<h3>{{ 'apps.noAccess.description' | tr }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length > 0">
|
||||
<h1 class="view-header">
|
||||
Your Apps
|
||||
{{ 'apps.title' | tr }}
|
||||
<div class="pull-right">
|
||||
<form class="form-inline">
|
||||
<input type="text" class="form-control" placeholder="Search Apps" id="appSearch" ng-model="appSearch" ng-show="installedApps.length > 10"/>
|
||||
<multiselect ng-model="selectedTags" ng-show="tags.length > 0" ms-header="All Tags" ms-selected="Tags: {{ selectedTags.join(', ') }}" options="tag for tag in tags" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<input type="text" class="form-control" ng-show="installedApps.length > 8" placeholder="{{ 'apps.searchPlaceholder' | tr }}" id="appSearch" ng-model="appSearch"/>
|
||||
<multiselect ng-model="selectedGroup" ng-show="user.isAtLeastAdmin && installedApps.length > 1 && 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 && installedApps.length > 1" 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" ng-show="filterDomains.length > 2" 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>
|
||||
</form>
|
||||
</div>
|
||||
@@ -77,42 +71,46 @@
|
||||
|
||||
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length > 0">
|
||||
<div class="app-grid">
|
||||
<div class="grid-item" ng-repeat="app in installedApps | selectedTagFilter:selectedTags | selectedDomainFilter:selectedDomain | appSearchFilter:appSearch | orderBy:'location'">
|
||||
<a ng-href="{{ app | applicationLink }}" ng-click="user.isAtLeastAdmin && (((app | installError) === true || (app | installationActive) === true) && showAppConfigure(app, 'repair')) || ((app | appIsInstalledAndHealthy) && app.pendingPostInstallConfirmation && appPostInstallConfirm.show(app))" target="_blank" ng-class="{ 'hand': (app | appIsInstalledAndHealthy) || (app | installError) || (app | installationActive)}">
|
||||
<div style="background-color: white;" class="highlight grid-item-content" uib-tooltip="{{ app.fqdn }}" tooltip-class="long nowrap">
|
||||
<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'" ng-class="{ 'admin-action': app.manifest.configurePath && (app | applicationLink) }">
|
||||
<div class="grid-item-content" uib-tooltip="{{ app.fqdn }}">
|
||||
<a ng-show="isOperator(app)" ng-href="#/app/{{ app.id}}/display" class="btn btn-lg btn-default grid-item-action"><i class="fas fa-cog"></i></a>
|
||||
<a ng-href="{{ app | applicationLink }}" ng-click="isOperator(app) && (((app | installError) === true || (app | installationActive) === true) && showAppConfigure(app, 'repair')) || ((app | appIsInstalledAndHealthy) && app.pendingPostInstallConfirmation && appPostInstallConfirm.show(app))" target="_blank" ng-class="{ 'hand': (app | appIsInstalledAndHealthy) || (app | installError) || (app | installationActive)}">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-center" style="padding-left: 5px; padding-right: 5px;">
|
||||
<br/>
|
||||
<img ng-src="{{ app.iconUrl || 'img/appicon_fallback.png' }}" fallback-icon="img/appicon_fallback.png" onerror="imageErrorHandler(this)" class="app-icon"/>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-center">
|
||||
<div class="grid-item-top-title" data-fittext>{{ app.label || app.location || app.fqdn }}</div>
|
||||
<div class="text-muted status" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden" uib-tooltip="{{ app | appProgressMessage }}" tooltip-class="long nowrap">
|
||||
{{ app | installationStateLabel:user }}
|
||||
<div class="grid-item-top-title" data-fittext>{{ app.label || app.subdomain || app.fqdn }}</div>
|
||||
<div class="text-muted status" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden" uib-tooltip="{{ app | appProgressMessage }}">
|
||||
{{ app | installationStateLabel }}
|
||||
</div>
|
||||
<div class="status" ng-style="{ 'visibility': user.isAtLeastAdmin && (app | installationActive) ? 'visible' : 'hidden' }">
|
||||
<div class="status" ng-style="{ 'visibility': isOperator(app) && (app | installationActive) ? 'visible' : 'hidden' }">
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ app.progress }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="usermanagement-indicator" ng-hide="user.isAtLeastAdmin">
|
||||
<i class="fas fa-cog" ng-show="isOperator(app)"></i>
|
||||
<i class="fas fa-user" ng-show="app.ssoAuth && !app.manifest.addons.email" uib-tooltip="{{ 'apps.auth.sso' | tr }}" tooltip-placement="right"></i>
|
||||
<i class="far fa-user" ng-show="!app.ssoAuth && !app.manifest.addons.email" uib-tooltip="{{ 'apps.auth.nosso' | tr }}" tooltip-placement="right"></i>
|
||||
<i class="fas fa-envelope" ng-show="app.manifest.addons.email" uib-tooltip="{{ 'apps.auth.email' | tr }}" tooltip-placement="right"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-item-actions" ng-show="user.isAtLeastAdmin">
|
||||
<a ng-href="#/app/{{ app.id}}/display" class="scale"><i class="fas fa-cogs"></i></a>
|
||||
</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.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version && (app | installSuccess)">
|
||||
<i class="fa fa-arrow-up fa-inverse"></i>
|
||||
</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)">
|
||||
<i class="fa fa-arrow-up fa-inverse"></i>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+73
-19
@@ -3,18 +3,37 @@
|
||||
/* global angular:false */
|
||||
/* global $:false */
|
||||
|
||||
angular.module('Application').controller('AppsController', ['$scope', '$timeout', '$interval', '$location', 'Client', function ($scope, $timeout, $interval, $location, Client) {
|
||||
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
|
||||
var GROUP_ACCESS_UNSET = { _unset: true, name: 'Select Group' }; // dummy record for the single select filter
|
||||
|
||||
$scope.installedApps = Client.getInstalledApps();
|
||||
$scope.tags = Client.getAppTags();
|
||||
$scope.states = [
|
||||
{ state: '', label: 'All States' },
|
||||
{ state: 'running', label: 'Running' },
|
||||
{ state: 'stopped', label: 'Stopped' },
|
||||
{ state: 'not_responding', label: 'Not Responding' }
|
||||
];
|
||||
$scope.selectedState = $scope.states[0];
|
||||
$scope.selectedTags = [];
|
||||
$scope.selectedGroup = GROUP_ACCESS_UNSET;
|
||||
$scope.selectedDomain = ALL_DOMAINS_DOMAIN;
|
||||
$scope.filterDomains = [ ALL_DOMAINS_DOMAIN ];
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.domains = [];
|
||||
$scope.appSearch = '';
|
||||
$scope.groups = [ GROUP_ACCESS_UNSET ];
|
||||
|
||||
$translate(['apps.stateFilterHeader', 'apps.domainsFilterHeader', 'apps.groupsFilterHeader', 'app.states.running', 'app.states.stopped', 'app.states.notResponding']).then(function (tr) {
|
||||
if (tr['apps.domainsFilterHeader']) ALL_DOMAINS_DOMAIN.domain = tr['apps.domainsFilterHeader'];
|
||||
if (tr['apps.groupsFilterHeader']) GROUP_ACCESS_UNSET.name = tr['apps.groupsFilterHeader'];
|
||||
if (tr['apps.stateFilterHeader']) $scope.states[0].label = tr['apps.stateFilterHeader'];
|
||||
if (tr['app.states.running']) $scope.states[1].label = tr['app.states.running'];
|
||||
if (tr['app.states.stopped']) $scope.states[2].label = tr['app.states.stopped'];
|
||||
if (tr['app.states.notResponding']) $scope.states[3].label = tr['app.states.notResponding'];
|
||||
});
|
||||
|
||||
$scope.$watch('selectedTags', function (newVal, oldVal) {
|
||||
if (newVal === oldVal) return;
|
||||
@@ -22,6 +41,20 @@ angular.module('Application').controller('AppsController', ['$scope', '$timeout'
|
||||
localStorage.selectedTags = newVal.join(',');
|
||||
});
|
||||
|
||||
$scope.$watch('selectedState', function (newVal, oldVal) {
|
||||
if (newVal === oldVal) return;
|
||||
|
||||
if (newVal === $scope.states[0]) localStorage.removeItem('selectedState');
|
||||
else localStorage.selectedState = newVal.state;
|
||||
});
|
||||
|
||||
$scope.$watch('selectedGroup', function (newVal, oldVal) {
|
||||
if (newVal === oldVal) return;
|
||||
|
||||
if (newVal === GROUP_ACCESS_UNSET) localStorage.removeItem('selectedGroup');
|
||||
else localStorage.selectedGroup = newVal.id;
|
||||
});
|
||||
|
||||
$scope.$watch('selectedDomain', function (newVal, oldVal) {
|
||||
if (newVal === oldVal) return;
|
||||
|
||||
@@ -58,32 +91,53 @@ angular.module('Application').controller('AppsController', ['$scope', '$timeout'
|
||||
$location.path('/app/' + app.id + '/' + view);
|
||||
};
|
||||
|
||||
$scope.isOperator = function (app) {
|
||||
return app.accessLevel === 'operator' || app.accessLevel === 'admin';
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
setTimeout(function () { $('#appSearch').focus(); }, 1);
|
||||
|
||||
// refresh the new list immediately when switching from another view (appstore)
|
||||
Client.refreshInstalledApps(function () {
|
||||
var refreshAppsTimer = $interval(Client.refreshInstalledApps.bind(Client, function () {}), 5000);
|
||||
$scope.$on('$destroy', function () {
|
||||
$interval.cancel(refreshAppsTimer);
|
||||
});
|
||||
|
||||
if (!$scope.user.isAtLeastAdmin) return;
|
||||
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) Client.error(error);
|
||||
|
||||
$scope.domains = result;
|
||||
$scope.filterDomains = [ALL_DOMAINS_DOMAIN].concat(result);
|
||||
|
||||
// load local settings and apply
|
||||
if (localStorage.selectedTags) {
|
||||
if (!$scope.tags.length) localStorage.removeItem('selectedTags');
|
||||
else $scope.selectedTags = localStorage.selectedTags.split(',');
|
||||
}
|
||||
if (localStorage.selectedDomain) $scope.selectedDomain = $scope.filterDomains.find(function (d) { return d.domain === localStorage.selectedDomain; }) || ALL_DOMAINS_DOMAIN;
|
||||
|
||||
setTimeout(function () { $('#appSearch').focus(); }, 1000);
|
||||
});
|
||||
});
|
||||
|
||||
if (!$scope.user.isAtLeastAdmin) return;
|
||||
|
||||
// load local settings and apply tag filter
|
||||
if (localStorage.selectedTags) {
|
||||
if (!$scope.tags.length) localStorage.removeItem('selectedTags');
|
||||
else $scope.selectedTags = localStorage.selectedTags.split(',');
|
||||
}
|
||||
|
||||
if (localStorage.selectedState) $scope.selectedState = $scope.states.find(function (s) { return s.state === localStorage.selectedState; }) || $scope.states[0];
|
||||
|
||||
Client.getGroups(function (error, result) {
|
||||
if (error) Client.error(error);
|
||||
|
||||
$scope.groups = [ GROUP_ACCESS_UNSET ].concat(result);
|
||||
|
||||
if (localStorage.selectedGroup) $scope.selectedGroup = $scope.groups.find(function (g) { return g.id === localStorage.selectedGroup; }) || GROUP_ACCESS_UNSET;
|
||||
});
|
||||
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) Client.error(error);
|
||||
|
||||
$scope.domains = result;
|
||||
$scope.filterDomains = [ALL_DOMAINS_DOMAIN].concat(result);
|
||||
|
||||
if (localStorage.selectedDomain) $scope.selectedDomain = $scope.filterDomains.find(function (d) { return d.domain === localStorage.selectedDomain; }) || ALL_DOMAINS_DOMAIN;
|
||||
});
|
||||
});
|
||||
|
||||
$('.collapse').on('shown.bs.collapse', function(){
|
||||
$(this).parent().find('.fa-angle-right').removeClass('fa-angle-right').addClass('fa-angle-down');
|
||||
}).on('hidden.bs.collapse', function(){
|
||||
$(this).parent().find('.fa-angle-down').removeClass('fa-angle-down').addClass('fa-angle-right');
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
|
||||
+211
-188
@@ -4,27 +4,26 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<img ng-src="{{appInstall.app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/>
|
||||
<h3 class="appstore-install-title" title="Version {{ appInstall.app.manifest.version }}">{{ appInstall.app.manifest.title }} <span class="badge badge-danger" ng-show="appInstall.app.publishState === 'testing'">Testing</span></h3>
|
||||
<img ng-src="{{appInstall.app.iconUrl}}" onerror="this.onerror=null; this.src='img/appicon_fallback.png'" class="app-icon"/>
|
||||
<h3 class="appstore-install-title">{{ appInstall.app.manifest.title }}</h3>
|
||||
<br/>
|
||||
<span class="appstore-install-meta"><a href="{{ appInstall.app.manifest.website }}" target="_blank">{{ appInstall.app.manifest.author }}</a></span>
|
||||
<br/>
|
||||
<span class="appstore-install-meta">Last updated {{ appInstall.app.creationDate | prettyDate }}</span>
|
||||
<span class="appstore-install-meta">{{ 'appstore.installDialog.lastUpdated' | tr:{ date: (appInstall.app.creationDate | prettyDate) } }}</span>
|
||||
<br/>
|
||||
<span class="appstore-install-meta hand">Requires atleast {{ appInstall.app.manifest.memoryLimit | prettyMemory }}MB memory</span>
|
||||
<span class="appstore-install-meta">{{ 'appstore.installDialog.memoryRequirement' | tr:{ size: (appInstall.app.manifest.memoryLimit | prettyByteSize:'256 MB') } }}</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="collapse" id="collapseInstallForm" data-toggle="false">
|
||||
<form role="form" name="appInstallForm" ng-submit="appInstall.submit()" autocomplete="off">
|
||||
<div class="has-error text-center" ng-show="appInstall.error.other" ng-bind-html="appInstall.error.other"></div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (appInstallForm.location.$dirty && appInstallForm.location.$invalid) || (!appInstallForm.location.$dirty && appInstall.error.location) }">
|
||||
<label class="control-label" for="appInstallLocationInput">Location</label>
|
||||
<div ng-show="appInstall.error.location"><small>{{ appInstall.error.location }}</small></div>
|
||||
<label class="control-label" for="appInstallLocationInput">{{ 'appstore.installDialog.location' | tr }}</label>
|
||||
<div class="input-group form-inline">
|
||||
<input type="text" class="form-control" ng-model="appInstall.location" id="appInstallLocationInput" name="location" placeholder="Leave empty to use bare domain" autofocus>
|
||||
<input type="text" class="form-control" ng-model="appInstall.subdomain" id="appInstallLocationInput" name="location" placeholder="{{ 'appstore.installDialog.locationPlaceholder' | tr }}" autofocus>
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<span>{{ (appInstall.location ? (appInstall.domain.config.hyphenatedSubdomains ? '-' : '.') : '') + appInstall.domain.domain }}</span>
|
||||
<span>{{ '.' + appInstall.domain.domain }}</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
@@ -34,12 +33,40 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="appInstall.error.location" class="text-small">{{ appInstall.error.location }}</div>
|
||||
</div>
|
||||
|
||||
<p class="text-center" ng-show="appInstall.location && appInstall.domain.provider === 'manual'">
|
||||
<b>Add an A record manually for {{ appInstall.location }} to this Cloudron's public IP</b>
|
||||
<br>
|
||||
</p>
|
||||
<p class="text-small text-warning" ng-show="appInstall.domain.provider === 'noop' || appInstall.domain.provider === 'manual'" ng-bind-html="'appstore.installDialog.manualWarning' | tr:{ location: ((appInstall.subdomain ? appInstall.subdomain + '.' : '') + appInstall.domain.domain) }"></p>
|
||||
|
||||
<div class="has-error text-center" ng-show="appInstall.error.secondaryDomain">{{ appInstall.error.secondaryDomain }}</div>
|
||||
<div ng-repeat="(env, info) in appInstall.app.manifest.httpPorts">
|
||||
<ng-form name="secondaryDomainInfo_form">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!secondaryDomainInfo_form.itemName{{$index}}.$dirty && appInstall.error.secondaryDomain) || (secondaryDomainInfo_form.itemName{{$index}}.$dirty && secondaryDomainInfo_form.itemName{{$index}}.$invalid) }">
|
||||
<label class="control-label" for="secondaryDomainInput{{env}}">
|
||||
{{ info.title }}
|
||||
<sup>
|
||||
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}}"><i class="fa fa-question-circle"></i></a>
|
||||
</sup>
|
||||
</label>
|
||||
|
||||
<div class="input-group form-inline">
|
||||
<input type="text" class="form-control" ng-model="appInstall.secondaryDomains[env].subdomain" name="location{{$index}}" placeholder="{{ 'appstore.installDialog.locationPlaceholder' | tr }}" autofocus>
|
||||
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<span>.{{ appInstall.secondaryDomains[env].domain.domain }}</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li ng-repeat="domain in domains">
|
||||
<a href="" ng-click="appInstall.secondaryDomains[env].domain = domain">{{ domain.domain }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-form>
|
||||
</div>
|
||||
|
||||
<div class="has-error text-center" ng-show="appInstall.error.port">{{ appInstall.error.port }}</div>
|
||||
<div ng-repeat="(env, info) in appInstall.portBindingsInfo">
|
||||
@@ -52,49 +79,51 @@
|
||||
</sup>
|
||||
</label>
|
||||
<input type="number" class="form-control" ng-model="appInstall.portBindings[env]" ng-disabled="!appInstall.portBindingsEnabled[env]" 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>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="appInstall.customAuth && !appInstall.app.manifest.addons.email">
|
||||
<label class="control-label">User management</label>
|
||||
<p>This app has it's own user management.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="appInstall.app.manifest.addons.email">
|
||||
<label class="control-label">User management</label>
|
||||
<p>All users with a mailbox on this Cloudron have access.</p>
|
||||
<label class="control-label">{{ 'appstore.installDialog.userManagement' | tr }}</label>
|
||||
<p>{{ 'appstore.installDialog.userManagementMailbox' | tr }}
|
||||
<span ng-bind-html="'appstore.installDialog.configuredForCloudronEmail' | tr:{ emailDocsLink: 'https://docs.cloudron.io/email/' }">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="!appInstall.customAuth && !appInstall.app.manifest.addons.email">
|
||||
<label class="control-label">User management</label>
|
||||
<div class="form-group">
|
||||
<label class="control-label" ng-show="!appInstall.customAuth && !appInstall.app.manifest.addons.email">{{ 'appstore.installDialog.userManagement' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#access-restriction" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<label class="control-label" ng-show="appInstall.customAuth || appInstall.app.manifest.addons.email">{{ 'app.accessControl.userManagement.dashboardVisibility' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#dashboard-visibility" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
|
||||
<div class="radio" ng-show="appInstall.optionalSso">
|
||||
<label>
|
||||
<input type="radio" ng-model="appInstall.accessRestrictionOption" value="nosso">
|
||||
Leave user management to the app
|
||||
<input type="radio" ng-model="appInstall.accessRestrictionOption" value="nosso"> {{ 'appstore.installDialog.userManagementLeaveToApp' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="appInstall.accessRestrictionOption" value="any">
|
||||
Allow all users from this Cloudron
|
||||
<span ng-show="!appInstall.customAuth">{{ 'appstore.installDialog.userManagementAllUsers' | tr }}</span>
|
||||
<span ng-show="appInstall.customAuth">{{ 'app.accessControl.userManagement.visibleForAllUsers' | tr }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="appInstall.accessRestrictionOption" value="groups">
|
||||
Only allow the following users and groups <span class="label label-danger" ng-show="appInstall.accessRestrictionOption === 'groups' && !appInstall.isAccessRestrictionValid()">Select at least one user or group</span>
|
||||
<span ng-show="!appInstall.customAuth">{{ 'appstore.installDialog.userManagementSelectUsers' | tr }}</span>
|
||||
<span ng-show="appInstall.customAuth">{{ 'app.accessControl.userManagement.visibleForSelected' | tr }}</span>
|
||||
<span class="label label-danger" ng-show="appInstall.accessRestrictionOption === 'groups' && !appInstall.isAccessRestrictionValid()">{{ 'appstore.installDialog.errorUserManagementSelectAtLeastOne' | tr }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div style="margin-left: 20px;">
|
||||
<div class="col-md-5">
|
||||
Users:
|
||||
{{ 'appstore.installDialog.users' | tr }}:
|
||||
<multiselect ng-model="appInstall.accessRestriction.users" ng-disabled="appInstall.accessRestrictionOption !== 'groups'" options="user.username for user in users" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5">
|
||||
Groups:
|
||||
{{ 'appstore.installDialog.groups' | tr }}:
|
||||
<multiselect ng-model="appInstall.accessRestriction.groups" ng-disabled="appInstall.accessRestrictionOption !== 'groups'" options="group.name for group in groups" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,14 +133,10 @@
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<p ng-show="appInstall.app.manifest.addons.email" class="text-info">
|
||||
This app is pre-configured for use with <a ng-href="{{ config.webServerOrigin }}/documentation/email/" target="_blank">Cloudron Email</a>.
|
||||
</p>
|
||||
|
||||
<div class="hide">
|
||||
<label class="control-label" for="appInstallCertificateInput" ng-show="appInstall.domain.provider !== 'caas'">Certificate (optional)</label>
|
||||
<div class="has-error text-center" ng-show="appInstall.error.cert && appInstall.domain.provider !== 'caas'">{{ appInstall.error.cert }}</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': !appInstallForm.certificate.$dirty && appInstall.error.cert }" ng-show="appInstall.domain.provider !== 'caas'">
|
||||
<label class="control-label" for="appInstallCertificateInput">Certificate (optional)</label>
|
||||
<div class="has-error text-center" ng-show="appInstall.error.cert">{{ appInstall.error.cert }}</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': !appInstallForm.certificate.$dirty && appInstall.error.cert }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="appInstallCertificateFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Certificate" ng-model="appInstall.certificateFileName" id="appInstallCertificateInput" name="certificate" onclick="getElementById('appInstallCertificateFileInput').click();" style="cursor: pointer;" ng-required="appInstall.keyFileName">
|
||||
@@ -120,7 +145,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': !appInstallForm.key.$dirty && appInstall.error.cert }" ng-show="appInstall.domain.provider !== 'caas'">
|
||||
<div class="form-group" ng-class="{ 'has-error': !appInstallForm.key.$dirty && appInstall.error.cert }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="appInstallKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Key" ng-model="appInstall.keyFileName" id="appInstallKeyInput" name="key" onclick="getElementById('appInstallKeyFileInput').click();" style="cursor: pointer;" ng-required="appInstall.certificateFileName">
|
||||
@@ -131,7 +156,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="(appInstall.accessRestrictionOption === 'groups' && !appInstall.isAccessRestrictionValid()) || appInstallForm.$invalid || busy"/>
|
||||
<input class="ng-hide" type="submit" ng-disabled="(appInstall.accessRestrictionOption === 'groups' && !appInstall.isAccessRestrictionValid()) || !appInstall.accessRestrictionOption || appInstallForm.$invalid || busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="collapse" id="collapseMediaLinksCarousel" data-toggle="false">
|
||||
@@ -139,24 +164,25 @@
|
||||
<slick init-onload="true" current-index="0" autoplay="true" arrows="false" autoplay-speed="2000" data="appInstall.mediaLinks" ng-show="appInstall.mediaLinks.length > 1">
|
||||
<div ng-repeat="mediaLink in appInstall.mediaLinks" class="slick-item" style="background-image: url('{{mediaLink}}');"></div>
|
||||
</slick>
|
||||
<br/>
|
||||
<div class="appstore-install-description">
|
||||
<div ng-bind-html="appInstall.app.manifest.description | markdown2html"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse" id="collapseResourceConstraint" data-toggle="false">
|
||||
<h4 class="text-danger">This Cloudron is running low on resources.</h4>
|
||||
<p>Please upgrade to a server instance with more memory. Alternately, free up resources by uninstalling unused applications.</p>
|
||||
<h4 class="text-danger">{{ 'appstore.installDialog.lowOnResources' | tr }}</h4>
|
||||
<p>{{ 'appstore.installDialog.pleaseUpgradeServer' | tr }}</p>
|
||||
</div>
|
||||
<div class="collapse" id="collapseSubscriptionRequired" data-toggle="false">
|
||||
<p class="text-bold">A subscription for this Cloudron is required to install more apps.</p>
|
||||
<p>{{ 'appstore.installDialog.subscriptionRequired' | tr }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-success" ng-click="openSubscriptionSetup()" ng-show="appInstall.state === 'subscriptionRequired'">Setup Subscription</button>
|
||||
<button type="button" class="btn btn-danger" ng-show="appInstall.state === 'resourceConstraint'" ng-click="appInstall.showForm(true)">Install anyway</button>
|
||||
<button type="button" class="btn btn-success" ng-show="appInstall.state === 'appInfo'" ng-click="appInstall.showForm()">Install</button>
|
||||
<button type="button" class="btn btn-success" ng-show="appInstall.state === 'installForm'" ng-click="appInstall.submit()" ng-disabled="(appInstall.accessRestrictionOption === 'groups' && !appInstall.isAccessRestrictionValid()) || appInstallForm.$invalid || appInstall.busy"><i class="fa fa-circle-notch fa-spin" ng-show="appInstall.busy"></i> Install {{ appInstall.needsOverwrite ? 'and overwrite DNS' : '' }}</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="openSubscriptionSetup()" ng-show="appInstall.state === 'subscriptionRequired'">{{ 'appstore.installDialog.setupSubscriptionAction' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-show="appInstall.state === 'resourceConstraint'" ng-click="appInstall.showForm(true)">{{ 'appstore.installDialog.installAnywayAction' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-show="appInstall.state === 'appInfo'" ng-click="appInstall.showForm()">{{ 'appstore.installDialog.installAction' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-show="appInstall.state === 'installForm'" ng-click="appInstall.submit()" ng-disabled="(appInstall.accessRestrictionOption === 'groups' && !appInstall.isAccessRestrictionValid()) || !appInstall.accessRestrictionOption || appInstallForm.$invalid || appInstall.busy"><i class="fa fa-circle-notch fa-spin" ng-show="appInstall.busy"></i> {{ 'appstore.installDialog.doInstallAction' | tr:{ dnsOverwrite: appInstall.needsOverwrite } }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -164,166 +190,163 @@
|
||||
|
||||
<!-- Modal app not found -->
|
||||
<div class="modal fade" id="appNotFoundModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">App not found</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
There is no such app <b>{{ appNotFound.appId }}</b><span ng-show="appNotFound.version"> with version <b>{{ appNotFound.version }}</b></span>.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'appstore.appNotFoundDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body" ng-bind-html="'appstore.appNotFoundDialog.description' | tr:{ appId: appNotFound.appId, version: appNotFound.version }"></div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="!ready" class="loading-banner">
|
||||
<h1><i class="fa fa-circle-notch fa-spin"></i></h1>
|
||||
<h1><i class="fa fa-circle-notch fa-spin"></i></h1>
|
||||
</div>
|
||||
|
||||
<!-- 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">Sign up with Cloudron.io</h1>
|
||||
<h1 ng-hide="appstoreLogin.register">Login to Cloudron.io</h1>
|
||||
</div>
|
||||
<div class="col-md-12 text-center">
|
||||
<p>This account is used to access the App Store and manage your subscription</p>
|
||||
</div>
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<small class="text-danger" ng-show="appstoreLogin.error.generic">{{ appstoreLogin.error.generic }}</small>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-md-12 text-center">
|
||||
<p>{{ 'appstore.accountDialog.description' | tr }}</p>
|
||||
</div>
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<small class="text-danger" ng-show="appstoreLogin.error.generic">{{ appstoreLogin.error.generic }}</small>
|
||||
</div>
|
||||
<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 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" 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>
|
||||
</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 }">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-hide="appstoreLogin.register" 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>
|
||||
</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>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<form name="appstoreLoginForm" role="form" novalidate ng-submit="appstoreLogin.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<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>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (appstoreLoginForm.email.$dirty && appstoreLoginForm.email.$invalid) || appstoreLogin.error.generic }">
|
||||
<label class="control-label">Email</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>
|
||||
</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 }">
|
||||
<label class="control-label">Password</label>
|
||||
<input type="password" class="form-control" ng-model="appstoreLogin.password" id="inputAppstoreLoginPassword" name="password" required>
|
||||
<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">Wrong password</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-hide="appstoreLogin.register" ng-class="{ 'has-error': appstoreLogin.error.totpToken }">
|
||||
<label class="control-label">2FA Token (if enabled)</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">Intended Use</label>
|
||||
<select class="purpose form-control" ng-model="appstoreLogin.purpose" required>
|
||||
<option value="" disabled selected hidden>Please choose an option...</option>
|
||||
<option value="personal_cloud">Personal use</option>
|
||||
<option value="business_cloud">Business use</option>
|
||||
<option value="website_hosting">Website hosting</option>
|
||||
<option value="paas">PaaS - Develop & deploy apps</option>
|
||||
<option value="single_app">Host only one app</option>
|
||||
<option value="exploring">Just exploring</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="appstoreLogin.termsAccepted">I accept the Cloudron <a href="https://cloudron.io/legal/license.html" target="_blank">license</a>
|
||||
</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">Login</span><span ng-show="appstoreLogin.register">Create Account</span>
|
||||
</button>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<a href="" ng-click="appstoreLogin.register = true" ng-hide="appstoreLogin.register">Don't have an account yet? Sign up</a>
|
||||
<a href="" ng-click="appstoreLogin.register = false" ng-show="appstoreLogin.register">Already have an account? Log in</a>
|
||||
</center>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- give more vertical spacing so the login form does not appear clipped -->
|
||||
<div ng-show="ready && !validSubscription">
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<div ng-show="ready && validSubscription" class="ng-cloak" id="appstoreGrid">
|
||||
<div class="col-md-2">
|
||||
<br/>
|
||||
<div>
|
||||
<form ng-submit="search()">
|
||||
<div class="input-group">
|
||||
<input type="text" id="appstoreSearch" class="form-control" style="height: 40px" placeholder="Search" ng-model="searchString" ng-change="search()" autofocus>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<br/>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'featured' }" category="featured">Popular</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'new' }" category="new">New Apps</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'recent' }" category="recent">Recently Updated</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === '' }" category="">All</a>
|
||||
<br/>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'analytics' }" category="analytics"><i class="fa fa-chart-line"></i> Analytics</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'blog' }" category="blog"><i class="fa fa-font"></i> Blog</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'chat' }" category="chat"><i class="fa fa-comments"></i> Chat</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'git' }" category="git"><i class="fa fa-code-branch"></i> Code Hosting</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'CRM' }" category="crm"><i class="fab fa-connectdevelop"></i> CRM</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'document' }" category="document"><i class="fa fa-file-word"></i> Documents</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'email' }" category="email"><i class="fa fa-envelope"></i> Email</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'sync' }" category="sync"><i class="fa fa-sync-alt"></i> File Sync</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'finance' }" category="finance"><i class="fa fa-hand-holding-usd"></i> Finance</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'forum' }" category="forum"><i class="fa fa-users"></i> Forum</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'gallery' }" category="gallery"><i class="fa fa-images"></i> Gallery</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'game' }" category="game"><i class="fa fa-gamepad"></i> Games</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'notes' }" category="notes"><i class="fa fa-sticky-note"></i> Notes</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'project' }" category="project"><i class="fas fa-project-diagram"></i> Project Management</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'vpn' }" category="vpn"><i class="fa fa-user-secret"></i> VPN</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'hosting' }" category="hosting"><i class="fa fa-server"></i> Web Hosting</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'wiki' }" category="wiki"><i class="fab fa-wikipedia-w"></i> Wiki</a>
|
||||
<br/>
|
||||
<br/>
|
||||
<a href="https://forum.cloudron.io/category/5/app-requests" target="_blank">Missing an app? Let us know.</a>
|
||||
</div>
|
||||
<div class="col-md-10" ng-show="apps.length">
|
||||
<div class="row-no-margin">
|
||||
<div class="col-sm-1 appstore-item" ng-repeat="app in apps | orderBy:'installCount':true">
|
||||
<div class="appstore-item-content highlight" ng-click="gotoApp(app)" ng-class="{ 'appstore-item-content-testing': app.releaseState === 'unstable' }">
|
||||
<span class="badge badge-danger appstore-item-badge-testing" ng-show="app.releaseState === 'unstable'">Unstable</span>
|
||||
<div class="appstore-item-content-icon col-same-height">
|
||||
<img ng-src="{{app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/>
|
||||
</div>
|
||||
<div class="appstore-item-content-description col-same-height">
|
||||
<h4 class="appstore-item-content-title">{{ app.manifest.title }}</h4>
|
||||
<div class="appstore-item-content-tagline text-muted">{{ app.manifest.tagline }}</div>
|
||||
<!-- <div class="appstore-item-rating"><i class="fa fa-star"></i><i class="fa fa-star"></i><i class="fa fa-star"></i><i class="fa fa-star-half-o"></i><i class="fa fa-star-o"></i></div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-10 animateMeOpacity loading-banner" ng-show="!apps.length">
|
||||
<h3 class="text-muted">No apps found.</h3>
|
||||
<a href="https://forum.cloudron.io/category/5/app-requests" target="_blank"><h3>Request an app or vote for one in our forum.</h3></a>
|
||||
<div ng-show="ready && validSubscription" class="ng-cloak appstore-toolbar">
|
||||
<div class="appstore-toolbar-content">
|
||||
<button class="btn" type="button" ng-click="showCategory('');" ng-class="{ 'btn-primary': '' === category }">{{ 'appstore.category.all' | tr }}</button>
|
||||
<button class="btn" type="button" ng-click="showCategory('new');" ng-class="{ 'btn-primary': 'new' === category }">{{ 'appstore.category.newApps' | tr }}</button>
|
||||
<div class="dropdown">
|
||||
<button class="btn dropdown-toggle" type="button" data-toggle="dropdown" ng-class="{ 'btn-primary': '' !== category && 'recent' !== category && 'new' !== category }">
|
||||
{{ categoryButtonLabel(category) }}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="category in categories | orderBy:'label'"><a href="" ng-click="showCategory(category.id);"><i class="{{ category.icon }} fa-fw"></i> {{ category.label }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<input type="text" id="appstoreSearch" class="form-control" placeholder="{{ 'appstore.searchPlaceholder' | tr }}" ng-model="searchString" ng-change="search()" autofocus>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="ready && validSubscription" class="ng-cloak appstore-grid">
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center" ng-hide="apps.length">
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<h3 class="text-muted">{{ 'appstore.noAppsFound' | tr }}</h3>
|
||||
<br/>
|
||||
<a href="https://forum.cloudron.io/category/5/app-requests" target="_blank">{{ 'appstore.appMissing' | tr }}</a>
|
||||
</div>
|
||||
<div class="col-md-12" ng-show="category === '' && popularApps.length">
|
||||
<div class="row-no-margin">
|
||||
<div class="col-sm-12">
|
||||
<h2>{{ 'appstore.category.popular' | tr }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-no-margin">
|
||||
<div class="col-sm-1 appstore-item" ng-repeat="app in popularApps">
|
||||
<div class="appstore-item-content highlight" ng-click="gotoApp(app)" ng-class="{ 'appstore-item-content-testing': app.releaseState === 'unstable' }">
|
||||
<span class="badge badge-danger appstore-item-badge-testing" ng-show="app.releaseState === 'unstable'">{{ 'appstore.unstable' | tr }}</span>
|
||||
<div class="appstore-item-content-icon col-same-height">
|
||||
<img ng-src="{{app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/>
|
||||
</div>
|
||||
<div class="appstore-item-content-description col-same-height">
|
||||
<h4 class="appstore-item-content-title">{{ app.manifest.title }}</h4>
|
||||
<div class="appstore-item-content-tagline text-muted">{{ app.manifest.tagline }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12" ng-show="apps.length">
|
||||
<div class="row-no-margin" ng-show="!category && !searchString">
|
||||
<div class="col-sm-12">
|
||||
<h2>{{ 'appstore.category.all' | tr }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-no-margin">
|
||||
<div class="col-sm-1 appstore-item" ng-repeat="app in apps">
|
||||
<div class="appstore-item-content highlight" ng-click="gotoApp(app)" ng-class="{ 'appstore-item-content-testing': app.releaseState === 'unstable' }">
|
||||
<span class="badge badge-danger appstore-item-badge-testing" ng-show="app.releaseState === 'unstable'">{{ 'appstore.unstable' | tr }}</span>
|
||||
<div class="appstore-item-content-icon col-same-height">
|
||||
<img ng-src="{{app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/>
|
||||
</div>
|
||||
<div class="appstore-item-content-description col-same-height">
|
||||
<h4 class="appstore-item-content-title">{{ app.manifest.title }}</h4>
|
||||
<div class="appstore-item-content-tagline text-muted">{{ app.manifest.tagline }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+201
-108
@@ -2,10 +2,12 @@
|
||||
|
||||
/* global angular:false */
|
||||
/* global $:false */
|
||||
/* global async */
|
||||
/* global ERROR */
|
||||
/* global RSTATES */
|
||||
/* global moment */
|
||||
|
||||
angular.module('Application').controller('AppStoreController', ['$scope', '$location', '$timeout', '$routeParams', 'Client', function ($scope, $location, $timeout, $routeParams, Client) {
|
||||
angular.module('Application').controller('AppStoreController', ['$scope', '$translate', '$location', '$timeout', '$routeParams', 'Client', function ($scope, $translate, $location, $timeout, $routeParams, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.HOST_PORT_MIN = 1024;
|
||||
@@ -13,6 +15,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
|
||||
$scope.ready = false;
|
||||
$scope.apps = [];
|
||||
$scope.popularApps = [];
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.users = [];
|
||||
@@ -39,21 +42,68 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
$('.modal').modal('hide');
|
||||
};
|
||||
|
||||
// If new categories added make sure the translation below exists
|
||||
$scope.categories = [
|
||||
{ id: 'analytics', icon: 'fa fa-chart-line', label: 'Analytics'},
|
||||
{ id: 'blog', icon: 'fa fa-font', label: 'Blog'},
|
||||
{ id: 'chat', icon: 'fa fa-comments', label: 'Chat'},
|
||||
{ id: 'crm', icon: 'fab fa-connectdevelop', label: 'CRM'},
|
||||
{ id: 'document', icon: 'fa fa-file-word', label: 'Documents'},
|
||||
{ id: 'email', icon: 'fa fa-envelope', label: 'Email'},
|
||||
{ id: 'federated', icon: 'fa fa-project-diagram', label: 'Federated'},
|
||||
{ id: 'finance', icon: 'fa fa-hand-holding-usd', label: 'Finance'},
|
||||
{ id: 'forum', icon: 'fa fa-users', label: 'Forum'},
|
||||
{ id: 'gallery', icon: 'fa fa-images', label: 'Gallery'},
|
||||
{ id: 'game', icon: 'fa fa-gamepad', label: 'Games'},
|
||||
{ id: 'git', icon: 'fa fa-code-branch', label: 'Code Hosting'},
|
||||
{ id: 'hosting', icon: 'fa fa-server', label: 'Web Hosting'},
|
||||
{ id: 'learning', icon: 'fas fa-graduation-cap', label: 'Learning'},
|
||||
{ id: 'media', icon: 'fas fa-photo-video', label: 'Media'},
|
||||
{ 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: 'vpn', icon: 'fa fa-user-secret', label: 'VPN'},
|
||||
{ id: 'wiki', icon: 'fab fa-wikipedia-w', label: 'Wiki'},
|
||||
];
|
||||
|
||||
// Translation IDs are generated as "appstore.category.<categoryId>"
|
||||
$translate($scope.categories.map(function (c) { return 'appstore.category.' + c.id; })).then(function (tr) {
|
||||
Object.keys(tr).forEach(function (key) {
|
||||
if (key === tr[key]) return; // missing translation use default label
|
||||
|
||||
var category = $scope.categories.find(function (c) { return key.endsWith(c.id); });
|
||||
if (category) category.label = tr[key];
|
||||
});
|
||||
});
|
||||
|
||||
$scope.categoryButtonLabel = function (category) {
|
||||
var categoryLabel = $translate.instant('appstore.categoryLabel');
|
||||
|
||||
if (category === 'new') return categoryLabel;
|
||||
if (category === 'recent') return categoryLabel;
|
||||
|
||||
var tmp = $scope.categories.find(function (c) { return c.id === category; });
|
||||
if (tmp) return tmp.label;
|
||||
|
||||
return categoryLabel;
|
||||
};
|
||||
|
||||
$scope.appInstall = {
|
||||
busy: false,
|
||||
state: 'appInfo',
|
||||
error: {},
|
||||
app: {},
|
||||
needsOverwrite: false,
|
||||
location: '',
|
||||
domain: null,
|
||||
subdomain: '',
|
||||
domain: null, // object and not the string
|
||||
secondaryDomains: {},
|
||||
portBindings: {},
|
||||
mediaLinks: [],
|
||||
certificateFile: null,
|
||||
certificateFileName: '',
|
||||
keyFile: null,
|
||||
keyFileName: '',
|
||||
accessRestrictionOption: 'any',
|
||||
accessRestrictionOption: '',
|
||||
accessRestriction: { users: [], groups: [] },
|
||||
customAuth: false,
|
||||
optionalSso: false,
|
||||
@@ -68,8 +118,9 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
$scope.appInstall.app = {};
|
||||
$scope.appInstall.error = {};
|
||||
$scope.appInstall.needsOverwrite = false;
|
||||
$scope.appInstall.location = '';
|
||||
$scope.appInstall.subdomain = '';
|
||||
$scope.appInstall.domain = null;
|
||||
$scope.appInstall.secondaryDomains = {};
|
||||
$scope.appInstall.portBindings = {};
|
||||
$scope.appInstall.state = 'appInfo';
|
||||
$scope.appInstall.mediaLinks = [];
|
||||
@@ -77,7 +128,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
$scope.appInstall.certificateFileName = '';
|
||||
$scope.appInstall.keyFile = null;
|
||||
$scope.appInstall.keyFileName = '';
|
||||
$scope.appInstall.accessRestrictionOption = 'any';
|
||||
$scope.appInstall.accessRestrictionOption = '';
|
||||
$scope.appInstall.accessRestriction = { users: [], groups: [] };
|
||||
$scope.appInstall.optionalSso = false;
|
||||
$scope.appInstall.customAuth = false;
|
||||
@@ -99,7 +150,11 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
var DEFAULT_MEMORY_LIMIT = 1024 * 1024 * 256;
|
||||
|
||||
var needed = app.manifest.memoryLimit || DEFAULT_MEMORY_LIMIT; // RAM+Swap
|
||||
var used = Client.getInstalledApps().reduce(function (prev, cur) { return prev + (cur.memoryLimit || cur.manifest.memoryLimit || DEFAULT_MEMORY_LIMIT); }, 0);
|
||||
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 available = (totalMemory || 0) - used;
|
||||
|
||||
@@ -126,15 +181,25 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
|
||||
$scope.appInstall.mediaLinks = $scope.appInstall.app.manifest.mediaLinks || [];
|
||||
$scope.appInstall.domain = $scope.domains.find(function (d) { return $scope.config.adminDomain === d.domain; }); // pre-select the adminDomain
|
||||
|
||||
$scope.appInstall.secondaryDomains = {};
|
||||
var httpPorts = $scope.appInstall.app.manifest.httpPorts || {};
|
||||
for (var env2 in httpPorts) {
|
||||
$scope.appInstall.secondaryDomains[env2] = {
|
||||
subdomain: httpPorts[env2].defaultValue || '',
|
||||
domain: $scope.appInstall.domain
|
||||
};
|
||||
}
|
||||
|
||||
$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
|
||||
|
||||
var manifest = app.manifest;
|
||||
$scope.appInstall.optionalSso = !!manifest.optionalSso;
|
||||
$scope.appInstall.customAuth = !(manifest.addons['ldap'] || manifest.addons['oauth']);
|
||||
$scope.appInstall.customAuth = !(manifest.addons['ldap'] || manifest.addons['proxyAuth']);
|
||||
|
||||
$scope.appInstall.accessRestrictionOption = 'any';
|
||||
$scope.appInstall.accessRestrictionOption = $scope.groups.length ? '' : 'any'; // make the user select an ACL conciously if groups are used
|
||||
$scope.appInstall.accessRestriction = { users: [], groups: [] };
|
||||
|
||||
// set default ports
|
||||
@@ -153,6 +218,14 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
$scope.appInstall.error.location = null;
|
||||
$scope.appInstall.error.port = null;
|
||||
|
||||
var secondaryDomains = {};
|
||||
for (var env2 in $scope.appInstall.secondaryDomains) {
|
||||
secondaryDomains[env2] = {
|
||||
subdomain: $scope.appInstall.secondaryDomains[env2].subdomain,
|
||||
domain: $scope.appInstall.secondaryDomains[env2].domain.domain
|
||||
};
|
||||
}
|
||||
|
||||
// only use enabled ports from portBindings
|
||||
var finalPortBindings = {};
|
||||
for (var env in $scope.appInstall.portBindings) {
|
||||
@@ -170,8 +243,9 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
|
||||
var data = {
|
||||
overwriteDns: $scope.appInstall.needsOverwrite,
|
||||
location: $scope.appInstall.location || '',
|
||||
subdomain: $scope.appInstall.subdomain || '',
|
||||
domain: $scope.appInstall.domain.domain,
|
||||
secondaryDomains: secondaryDomains,
|
||||
portBindings: finalPortBindings,
|
||||
accessRestriction: finalAccessRestriction,
|
||||
cert: $scope.appInstall.certificateFile,
|
||||
@@ -179,30 +253,63 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
sso: !$scope.appInstall.optionalSso ? undefined : ($scope.appInstall.accessRestrictionOption !== 'nosso')
|
||||
};
|
||||
|
||||
Client.checkDNSRecords(data.domain, data.location, function (error, result) {
|
||||
if (error) return Client.error(error);
|
||||
var domains = [];
|
||||
domains.push({ subdomain: data.subdomain, domain: data.domain, type: 'primary' });
|
||||
var canInstall = true;
|
||||
|
||||
if (!data.overwriteDns) {
|
||||
if (result.error || result.needsOverwrite) {
|
||||
if (result.error) {
|
||||
if (result.error.reason === ERROR.ACCESS_DENIED) {
|
||||
$scope.appInstall.error.location = 'DNS credentials for ' + data.domain + ' are invalid. Update it in Domains & Certs view';
|
||||
async.eachSeries(domains, function (domain, callback) {
|
||||
if (data.overwriteDns) return callback();
|
||||
|
||||
Client.checkDNSRecords(domain.domain, domain.subdomain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var message;
|
||||
if (result.error) {
|
||||
if (result.error.reason === ERROR.ACCESS_DENIED) {
|
||||
message = 'DNS credentials for ' + domain.domain + ' are invalid. Update it in Domains & Certs view';
|
||||
if (domain.type === 'primary') {
|
||||
$scope.appInstall.error.location = message;
|
||||
} else {
|
||||
$scope.appInstall.error.location = result.error.message;
|
||||
$scope.appInstall.error.secondaryDomain = message;
|
||||
}
|
||||
} else {
|
||||
$scope.appInstall.error.location = 'DNS Record already exists. Confirm that the domain is not in use for services external to Cloudron';
|
||||
$scope.appInstall.needsOverwrite = true;
|
||||
if (domain.type === 'primary') {
|
||||
$scope.appInstall.error.location = result.error.message;
|
||||
} else {
|
||||
$scope.appInstall.error.secondaryDomain = message;
|
||||
}
|
||||
}
|
||||
$scope.appInstall.busy = false;
|
||||
$scope.appInstallForm.location.$setPristine();
|
||||
$('#appInstallLocationInput').focus();
|
||||
return;
|
||||
canInstall = false;
|
||||
} else if (result.needsOverwrite) {
|
||||
message = 'DNS Record already exists. Confirm that the domain is not in use for services external to Cloudron';
|
||||
if (data.type === 'primary') {
|
||||
$scope.appInstall.error.location = message;
|
||||
} else {
|
||||
$scope.appInstall.error.secondaryDomain = message;
|
||||
}
|
||||
$scope.appInstall.needsOverwrite = true;
|
||||
canInstall = false;
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) {
|
||||
$scope.location.busy = false;
|
||||
return Client.error(error);
|
||||
}
|
||||
|
||||
if (!canInstall) {
|
||||
$scope.appInstall.busy = false;
|
||||
$scope.appInstallForm.location.$setPristine();
|
||||
$('#appInstallLocationInput').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
Client.installApp($scope.appInstall.app.id, $scope.appInstall.app.manifest, $scope.appInstall.app.title, data, function (error, newAppId) {
|
||||
if (error) {
|
||||
var errorMessage = error.message.toLowerCase();
|
||||
|
||||
if (error.statusCode === 402) {
|
||||
$scope.appInstall.state = 'subscriptionRequired';
|
||||
$scope.appInstall.subscriptionErrorMesssage = error.message;
|
||||
@@ -211,17 +318,21 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
$('#collapseInstallForm').collapse('hide');
|
||||
$('#collapseSubscriptionRequired').collapse('show');
|
||||
} else if (error.statusCode === 409) {
|
||||
if (error.portName) {
|
||||
if (errorMessage.indexOf('port') !== -1) {
|
||||
$scope.appInstall.error.port = error.message;
|
||||
} else if (error.domain) {
|
||||
$scope.appInstall.error.location = error.message;
|
||||
$scope.appInstallForm.location.$setPristine();
|
||||
$('#appInstallLocationInput').focus();
|
||||
} else if (errorMessage.indexOf('location') !== -1) {
|
||||
if (errorMessage.indexOf('primary') !== -1) {
|
||||
$scope.appInstall.error.location = error.message;
|
||||
$scope.appInstallForm.location.$setPristine();
|
||||
$('#appInstallLocationInput').focus();
|
||||
} else {
|
||||
$scope.appInstall.error.secondaryDomain = error.message;
|
||||
}
|
||||
} else {
|
||||
$scope.appInstall.error.other = error.message;
|
||||
}
|
||||
} else if (error.statusCode === 400) {
|
||||
if (error.field === 'cert') {
|
||||
if (errorMessage.indexOf('cert') !== -1) {
|
||||
$scope.appInstall.error.cert = error.message;
|
||||
$scope.appInstall.certificateFileName = '';
|
||||
$scope.appInstall.certificateFile = null;
|
||||
@@ -275,13 +386,12 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
totpToken: '',
|
||||
register: true,
|
||||
termsAccepted: false,
|
||||
purpose: '',
|
||||
|
||||
submit: function () {
|
||||
$scope.appstoreLogin.error = {};
|
||||
$scope.appstoreLogin.busy = true;
|
||||
|
||||
Client.registerCloudron($scope.appstoreLogin.email, $scope.appstoreLogin.password, $scope.appstoreLogin.totpToken, $scope.appstoreLogin.register, $scope.appstoreLogin.purpose, function (error) {
|
||||
Client.registerCloudron($scope.appstoreLogin.email, $scope.appstoreLogin.password, $scope.appstoreLogin.totpToken, $scope.appstoreLogin.register, function (error) {
|
||||
if (error) {
|
||||
$scope.appstoreLogin.busy = false;
|
||||
|
||||
@@ -325,46 +435,15 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
return;
|
||||
}
|
||||
|
||||
getSubscription(function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
onSubscribed(function (error) { if (error) console.error(error); });
|
||||
});
|
||||
// do a full re-init of the view now that we have a subscription
|
||||
init();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function onSubscribed(callback) {
|
||||
Client.getAppstoreApps(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// start with featured apps listing. this also sets $scope.apps accordingly
|
||||
$scope.showCategory(null, 'featured');
|
||||
|
||||
// do this in background
|
||||
fetchUsers();
|
||||
fetchGroups();
|
||||
|
||||
// domains is required since we populate the dropdown with domains[0]
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
$scope.domains = result;
|
||||
|
||||
// show install app dialog immediately if an app id was passed in the query
|
||||
// hashChangeListener calls $apply, so make sure we don't double digest here
|
||||
setTimeout(hashChangeListener, 1);
|
||||
|
||||
setTimeout(function () { $('#appstoreSearch').focus(); }, 1000);
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// TODO does not support testing apps in search
|
||||
$scope.search = function () {
|
||||
if (!$scope.searchString) return $scope.showCategory(null, $scope.cachedCategory);
|
||||
if (!$scope.searchString) return $scope.showCategory($scope.cachedCategory);
|
||||
|
||||
$scope.category = '';
|
||||
|
||||
@@ -373,6 +452,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
|
||||
var token = $scope.searchString.toUpperCase();
|
||||
|
||||
$scope.popularApps = [];
|
||||
$scope.apps = apps.filter(function (app) {
|
||||
if (app.manifest.id.toUpperCase().indexOf(token) !== -1) return true;
|
||||
if (app.manifest.title.toUpperCase().indexOf(token) !== -1) return true;
|
||||
@@ -385,55 +465,54 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
};
|
||||
|
||||
function filterForNewApps(apps) {
|
||||
var minApps = apps.length < 5 ? apps.length : 5; // prevent endless loop
|
||||
var minApps = apps.length < 12 ? apps.length : 12; // prevent endless loop
|
||||
var tmp = [];
|
||||
var i = 0;
|
||||
|
||||
do {
|
||||
var offset = moment().subtract(i++, 'days');
|
||||
tmp = apps.filter(function (app) { return moment(app.ts).isAfter(offset); });
|
||||
tmp = apps.filter(function (app) { return moment(app.ts).isAfter(offset); }); // ts here is from appstore's apps table
|
||||
} while(tmp.length < minApps);
|
||||
|
||||
return tmp;
|
||||
}
|
||||
|
||||
function filterForRecentlyUpdatedApps(apps) {
|
||||
var minApps = apps.length < 5 ? apps.length : 5; // prevent endless loop
|
||||
var minApps = apps.length < 12 ? apps.length : 12; // prevent endless loop
|
||||
var tmp = [];
|
||||
var i = 0;
|
||||
|
||||
do {
|
||||
var offset = moment().subtract(i++, 'days');
|
||||
tmp = apps.filter(function (app) { return moment(app.creationDate).isAfter(offset); });
|
||||
tmp = apps.filter(function (app) { return moment(app.creationDate).isAfter(offset); }); // creationDate here is from appstore's appversions table
|
||||
} while(tmp.length < minApps);
|
||||
|
||||
return tmp;
|
||||
}
|
||||
|
||||
$scope.showCategory = function (event, category) {
|
||||
if (!event) $scope.category = category;
|
||||
else $scope.category = event.target.getAttribute('category');
|
||||
$scope.showCategory = function (category) {
|
||||
$scope.category = category;
|
||||
|
||||
$scope.cachedCategory = $scope.category;
|
||||
|
||||
Client.getAppstoreAppsFast(function (error, apps) {
|
||||
if (error) return $timeout($scope.showCategory.bind(null, event), 1000);
|
||||
if (error) return $timeout($scope.showCategory.bind(null, category), 1000);
|
||||
|
||||
if (!$scope.category) {
|
||||
$scope.apps = apps;
|
||||
} else if ($scope.category === 'featured') {
|
||||
$scope.apps = apps.filter(function (app) { return app.featured; });
|
||||
$scope.apps = apps.slice(0).filter(function (app) { return !app.featured; }).sort(function (a1, a2) { return a1.manifest.title.localeCompare(a2.manifest.title); });
|
||||
$scope.popularApps = apps.slice(0).filter(function (app) { return app.featured; }).sort(function (a1, a2) { return a2.ranking - a1.ranking; });
|
||||
} else if ($scope.category === 'new') {
|
||||
$scope.apps = filterForNewApps(apps);
|
||||
} else if ($scope.category === 'recent') {
|
||||
$scope.apps = filterForRecentlyUpdatedApps(apps);
|
||||
} else {
|
||||
$scope.apps = apps.filter(function (app) {
|
||||
return app.manifest.tags.some(function (tag) { return $scope.category === tag; });
|
||||
});
|
||||
return app.manifest.tags.some(function (tag) { return $scope.category === tag; }); // reverse sort;
|
||||
}).sort(function (a1, a2) { return a2.ranking - a1.ranking; });
|
||||
}
|
||||
|
||||
if (document.getElementById('appstoreGrid')) document.getElementById('appstoreGrid').scrollIntoView();
|
||||
// ensure we scroll to top
|
||||
document.getElementById('ng-view').scrollTop = 0;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -475,7 +554,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
|
||||
$scope.showAppNotFound = function (appId, version) {
|
||||
$scope.appNotFound.appId = appId;
|
||||
$scope.appNotFound.version = version;
|
||||
$scope.appNotFound.version = version || 'latest';
|
||||
|
||||
$('#appNotFoundModal').modal('show');
|
||||
};
|
||||
@@ -507,7 +586,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
}
|
||||
|
||||
function fetchUsers() {
|
||||
Client.getUsers(function (error, users) {
|
||||
Client.getAllUsers(function (error, users) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return $timeout(fetchUsers, 5000);
|
||||
@@ -528,6 +607,17 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
});
|
||||
}
|
||||
|
||||
function fetchMemory() {
|
||||
Client.memory(function (error, memory) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return $timeout(fetchMemory, 5000);
|
||||
}
|
||||
|
||||
$scope.memory = memory;
|
||||
});
|
||||
}
|
||||
|
||||
function getSubscription(callback) {
|
||||
Client.getSubscription(function (error, subscription) {
|
||||
if (error) {
|
||||
@@ -553,43 +643,46 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
});
|
||||
}
|
||||
|
||||
function getMemory(callback) {
|
||||
Client.memory(function (error, memory) {
|
||||
if (error) console.error(error);
|
||||
|
||||
$scope.memory = memory;
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
$scope.ready = false;
|
||||
Client.getAppstoreAppsFast(function (error) {
|
||||
$scope.ready = true;
|
||||
|
||||
getSubscription(function (error) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
if (error && error.statusCode === 402) {
|
||||
$scope.validSubscription = false;
|
||||
return;
|
||||
} else if (error) {
|
||||
console.error('Failed to get apps. Will retry.', error);
|
||||
return $timeout(init, 1000);
|
||||
}
|
||||
|
||||
if (!$scope.validSubscription) { // show the login form
|
||||
$scope.ready = true;
|
||||
return;
|
||||
}
|
||||
$scope.validSubscription = true;
|
||||
|
||||
onSubscribed(function (error) {
|
||||
if (error) console.error(error);
|
||||
$scope.showCategory('');
|
||||
|
||||
$scope.ready = true;
|
||||
// refresh everything in background
|
||||
getSubscription(function (error) { if (error) console.error('Failed to get subscription.', error); });
|
||||
Client.getAppstoreApps(function (error) { if (error) console.error('Failed to fetch apps.', error); });
|
||||
Client.refreshConfig(); // refresh domain, user, group limit etc
|
||||
fetchUsers();
|
||||
fetchGroups();
|
||||
fetchMemory();
|
||||
|
||||
// domains is required since we populate the dropdown with domains[0]
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) return console.error('Error getting domains.', error);
|
||||
|
||||
$scope.domains = result;
|
||||
|
||||
// show install app dialog immediately if an app id was passed in the query
|
||||
// hashChangeListener calls $apply, so make sure we don't double digest here
|
||||
setTimeout(hashChangeListener, 1);
|
||||
|
||||
setTimeout(function () { $('#appstoreSearch').focus(); }, 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Client.onReady(function () {
|
||||
getMemory(function () {
|
||||
init();
|
||||
});
|
||||
});
|
||||
Client.onReady(init);
|
||||
|
||||
// note: do not use hide.bs.model since it is called immediately from switchToAppsView which is already in angular scope
|
||||
$('#appInstallModal').on('hidden.bs.modal', function () {
|
||||
|
||||
+477
-178
@@ -1,15 +1,108 @@
|
||||
<!-- Modal details -->
|
||||
<div class="modal fade" id="backupDetailsModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'backups.backupDetails.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-xs-3 text-muted">{{ 'backups.backupDetails.id' | tr }}:</div>
|
||||
<div class="col-xs-9 text-right">{{ backupDetails.backup.id }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-3 text-muted">{{ 'backups.backupDetails.date' | tr }}:</div>
|
||||
<div class="col-xs-9 text-right">{{ backupDetails.backup.creationTime | prettyLongDate }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-3 text-muted">{{ 'backups.backupDetails.version' | tr }}:</div>
|
||||
<div class="col-xs-9 text-right">v{{ backupDetails.backup.packageVersion }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-3 text-muted">{{ 'backups.backupDetails.format' | tr }}:</div>
|
||||
<div class="col-xs-9 text-right">{{ backupDetails.backup.format }}</div>
|
||||
</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>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal backup failed -->
|
||||
<div class="modal fade" id="createBackupFailedModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Unable to create backup</h4>
|
||||
<h4 class="modal-title">{{ 'backups.backupFailed.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ createBackup.errorMessage }}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-success" data-dismiss="modal">OK</button>
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cleanup backups info -->
|
||||
<div class="modal fade" id="cleanupBackupsModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'backups.cleanupBackups.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">{{ 'backups.cleanupBackups.description' | tr }}</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="createBackup.startCleanup()">{{ 'backups.cleanupBackups.cleanupNow' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- modal backup config -->
|
||||
<div class="modal fade" id="configureScheduleAndRetentionModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'backups.configureBackupSchedule.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="configureScheduleAndRetentionForm" role="form" novalidate ng-submit="configureScheduleAndRetention.submit()" autocomplete="off">
|
||||
<p class="has-error text-center" ng-show="configureScheduleAndRetention.error">{{ configureScheduleAndRetention.error.generic }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="backupSchedule">{{ 'backups.configureBackupSchedule.schedule' | tr }}</label>
|
||||
<p ng-bind-html="'backups.configureBackupSchedule.scheduleDescription' | tr"></p>
|
||||
|
||||
<div class="row" style="margin-left: 20px;">
|
||||
<div class="col-md-5">
|
||||
{{ 'backups.configureBackupSchedule.days' | tr }}: <multiselect id="backupSchedule" class="input-sm stretch" ng-model="configureScheduleAndRetention.days" options="a.name for a in cronDays" data-multiple="true"></multiselect>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5">
|
||||
{{ 'backups.configureBackupSchedule.hours' | tr }}: <multiselect class="input-sm stretch" ng-model="configureScheduleAndRetention.hours" options="a.name for a in cronHours" data-multiple="true"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="backupRetention">{{ 'backups.configureBackupSchedule.retentionPolicy' | tr }}</label>
|
||||
<select class="form-control" id="backupRetention" ng-model="configureScheduleAndRetention.retentionPolicy" ng-options="a.value as a.name for a in retentionPolicies"></select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="configureScheduleAndRetention.submit()" ng-disabled="configureScheduleAndRetention.$invalid || configureScheduleAndRetention.busy"><i class="fa fa-circle-notch fa-spin" ng-show="configureScheduleAndRetention.busy"></i><span> {{ 'main.dialog.save' | tr }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -20,150 +113,272 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Configure Backup Storage</h4>
|
||||
<h4 class="modal-title">{{ 'backups.configureBackupStorage.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Cloudron makes a complete backup of your system based on this configuration.</p>
|
||||
|
||||
<form name="configureBackupForm" role="form" novalidate ng-submit="configureBackup.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<p class="has-error text-center" ng-show="configureBackup.error">{{ configureBackup.error.generic }}</p>
|
||||
<p class="has-error text-center" ng-show="configureBackup.error">{{ configureBackup.error.generic }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="storageProviderProvider">{{ 'backups.configureBackupStorage.provider' | tr }} <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>
|
||||
<p class="small text-info" ng-show="backupConfig.provider !== configureBackup.provider">Backups in the old storage location have to be removed manually.</p>
|
||||
<select class="form-control" id="storageProviderProvider" ng-model="configureBackup.provider" ng-options="a.value as a.name for a in storageProvider" ng-change=configureBackup.clearProviderFields()></select>
|
||||
</div>
|
||||
|
||||
<!-- Noop -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'noop'">
|
||||
<p class="has-error">{{ 'backups.configureBackupStorage.noopNote' | tr }}</p>
|
||||
</div>
|
||||
|
||||
<!-- mountpoint -->
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'cifs' || configureBackup.provider === 'nfs' || configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupHost">{{ 'backups.configureBackupStorage.server' | tr }} ({{ configureBackup.provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.host" id="configureBackupHost" name="host" ng-disabled="configureBackup.busy" placeholder="Server IP or hostname" ng-required="configureBackup.provider === 'cifs' || configureBackup.provider === 'nfs' || configureBackup.provider === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'cifs'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.mountOptions.seal">{{ 'backups.configureBackupStorage.cifsSealSupport' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'cifs' || configureBackup.provider === 'nfs' || configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupRemoteDir">{{ 'backups.configureBackupStorage.remoteDirectory' | tr }} ({{ configureBackup.provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.remoteDir" id="configureBackupRemoteDir" name="remoteDir" ng-disabled="configureBackup.busy" placeholder="/share" ng-required="configureBackup.provider === 'cifs' || configureBackup.provider === 'nfs' || configureBackup.provider === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'cifs'">
|
||||
<label class="control-label" for="configureBackupUsername">{{ 'backups.configureBackupStorage.username' | tr }} ({{ configureBackup.provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.username" id="configureBackupUsername" name="username" ng-disabled="configureBackup.busy">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'cifs'">
|
||||
<label class="control-label" for="configureBackupPassword">{{ 'backups.configureBackupStorage.password' | tr }} ({{ configureBackup.provider }})</label>
|
||||
<input type="password" class="form-control" ng-model="configureBackup.mountOptions.password" id="configureBackupPassword" name="password" ng-disabled="configureBackup.busy" password-reveal>
|
||||
</div>
|
||||
|
||||
<!-- EXT4 -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.diskPath || !configureBackup.mountOptions.diskPath }" ng-show="configureBackup.provider === 'ext4'">
|
||||
<label class="control-label" for="inputConfigureDiskPath">{{ 'backups.configureBackupStorage.diskPath' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.diskPath" id="inputConfigureDiskPath" name="diskPath" ng-disabled="configureBackup.busy" placeholder="/dev/disk/by-uuid/uuid" ng-required="configureBackup.provider === 'ext4'">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupPort">{{ 'backups.configureBackupStorage.port' | tr }}</label>
|
||||
<input type="number" class="form-control" ng-model="configureBackup.mountOptions.port" id="configureBackupPort" name="port" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupUser">{{ 'backups.configureBackupStorage.user' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.user" id="configureBackupUser" name="user" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupPrivateKey">{{ 'backups.configureBackupStorage.privateKey' | tr }}</label>
|
||||
<textarea class="form-control" ng-model="configureBackup.mountOptions.privateKey" id="configureBackupPrivateKey" name="privateKey" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'sshfs'"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Filesystem -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.backupFolder }" ng-show="configureBackup.provider === 'filesystem'">
|
||||
<label class="control-label" for="inputConfigureBackupFolder">{{ 'backups.configureBackupStorage.localDirectory' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.backupFolder" id="inputConfigureBackupFolder" name="backupFolder" ng-disabled="configureBackup.busy" placeholder="Directory for backups" ng-required="configureBackup.provider === 'filesystem'">
|
||||
</div>
|
||||
|
||||
<!-- Filesystem/SSHFS/CIFS/NFS/EXT4/mountpoint -->
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'filesystem' || mountlike(configureBackup.provider)">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.useHardlinks">{{ 'backups.configureBackupStorage.hardlinksLabel' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- mountpoint -->
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'mountpoint'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.chown">{{ 'backups.configureBackupStorage.chown' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- S3/Minio/SOS/GCS/UpCloud -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.endpoint }" ng-show="configureBackup.provider === 'minio' || configureBackup.provider === 'upcloud-objectstorage' || configureBackup.provider === 'backblaze-b2' || configureBackup.provider === 's3-v4-compat'">
|
||||
<label class="control-label" for="inputConfigureBackupEndpoint">{{ 'backups.configureBackupStorage.s3Endpoint' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.endpoint" id="inputConfigureBackupEndpoint" name="endpoint" ng-disabled="configureBackup.busy" placeholder="URL" ng-required="configureBackup.provider === 'minio' || configureBackup.provider === 'upcloud-objectstorage' || configureBackup.provider === 'backblaze-b2' || configureBackup.provider === 's3-v4-compat'">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'minio' || configureBackup.provider === 's3-v4-compat'" >
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.acceptSelfSignedCerts">{{ 'backups.configureBackupStorage.acceptSelfSignedCerts' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.bucket }" ng-show="s3like(configureBackup.provider) || configureBackup.provider === 'gcs'">
|
||||
<label class="control-label" for="inputConfigureBackupBucket">{{ 'backups.configureBackupStorage.bucketName' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.bucket" id="inputConfigureBackupBucket" name="bucket" ng-disabled="configureBackup.busy" ng-required="s3like(configureBackup.provider)">
|
||||
</div>
|
||||
|
||||
<!-- S3/Minio/SOS/GCS/SSHFS/CIFS/NFS/B2 -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.prefix }" ng-show="configureBackup.provider !== 'filesystem' && configureBackup.provider !== 'noop'">
|
||||
<label class="control-label" for="inputConfigureBackupPrefix">{{ 'backups.configureBackupStorage.prefix' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.prefix" id="inputConfigureBackupPrefix" name="prefix" ng-disabled="configureBackup.busy" placeholder="Prefix for backup file names">
|
||||
</div>
|
||||
|
||||
<!-- S3/Minio/SOS/GCS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 's3'">
|
||||
<label class="control-label" for="inputConfigureBackupS3Region">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupS3Region" ng-model="configureBackup.region" ng-options="a.value as a.name for a in s3Regions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 's3'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 's3-v4-compat'">
|
||||
<label class="control-label" for="inputConfigureBackupS3V4CompatRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<input class="form-control" type="text" name="region" id="inputConfigureBackupS3V4CompatRegion" ng-model="configureBackup.region" ng-disabled="configureBackup.busy" placeholder="Leave empty to use us-east-1 as default"></input>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'digitalocean-spaces'">
|
||||
<label class="control-label" for="inputConfigureBackupDORegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupDORegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in doSpacesRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'digitalocean-spaces'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'exoscale-sos'">
|
||||
<label class="control-label" for="inputConfigureBackupExoscaleRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupExoscaleRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in exoscaleSosRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'exoscale-sos'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'wasabi'">
|
||||
<label class="control-label" for="inputConfigureBackupWasabiRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupWasabiRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in wasabiRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'wasabi'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'scaleway-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupScalewayRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupScalewayRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in scalewayRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'scaleway-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'linode-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupLinodeRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupLinodeRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in linodeRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'linode-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'ovh-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupOvhRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupOvhRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in ovhRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'ovh-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'ionos-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupIonosRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupIonosRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in ionosRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'ionos-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'vultr-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupVultrRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupVultrRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in vultrRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'vultr-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.accessKeyId }" ng-show="s3like(configureBackup.provider)">
|
||||
<label class="control-label" for="inputConfigureBackupAccessKeyId">{{ 'backups.configureBackupStorage.s3AccessKeyId' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.accessKeyId" id="inputConfigureBackupAccessKeyId" name="accessKeyId" ng-disabled="configureBackup.busy" ng-required="s3like(configureBackup.provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.secretAccessKey }" ng-show="s3like(configureBackup.provider)">
|
||||
<label class="control-label" for="inputConfigureBackupSecretAccessKey">{{ 'backups.configureBackupStorage.s3SecretAccessKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.secretAccessKey" id="inputConfigureBackupSecretAccessKey" name="secretAccessKey" ng-disabled="configureBackup.busy" ng-required="s3like(configureBackup.provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.gcsKeyInput }" ng-show="configureBackup.provider === 'gcs'">
|
||||
<label class="control-label" for="gcsKeyInput">{{ 'backups.configureBackupStorage.gcsServiceKey' | tr }}</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="configureBackup.gcsKey.keyFileName" id="gcsKeyInput" name="cert" onclick="getElementById('gcsKeyFileInput').click();" style="cursor: pointer;" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'gcs'">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('gcsKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="configureBackup.provider !== 'noop'">
|
||||
<label class="control-label" for="storageFormat">{{ 'backups.configureBackupStorage.format' | tr }} <sup><a ng-href="https://docs.cloudron.io/backups/#backup-formats" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<p class="small text-info" ng-show="backupConfig.format !== configureBackup.format">{{ 'backups.configureBackupStorage.formatChangeNote' | tr }}</p>
|
||||
<p class="small text-info" ng-show="configureBackup.format === 'rsync' && (s3like(configureBackup.provider) || configureBackup.provider === 'gcs')">{{ 'backups.configureBackupStorage.s3LikeNote' | tr }} <sup><a ng-href="https://docs.cloudron.io/backups/#amazon-s3" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
|
||||
<select class="form-control" id="storageFormat" ng-model="configureBackup.format" ng-options="a.value as a.name for a in formats"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.password }" ng-show="configureBackup.provider !== 'noop'">
|
||||
<label class="control-label" for="inputConfigureBackupPassword">{{ 'backups.configureBackupStorage.encryptionPassword' | tr }} <sup><a ng-href="https://docs.cloudron.io/backups/#encryption" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.encryptionDescription' | tr }}</p>
|
||||
<input type="text" class="form-control" name="password" ng-model="configureBackup.password" id="inputConfigureBackupPassword" ng-disabled="configureBackup.busy" placeholder="{{ 'backups.configureBackupStorage.encryptionPasswordPlaceholder' | tr }}">
|
||||
|
||||
<div ng-show="configureBackup.password && configureBackup.password !== SECRET_PLACEHOLDER" ng-class="{ 'has-error': (configureBackupForm.password.$dirty && configureBackup.password !== configureBackup.passwordRepeat) }">
|
||||
<p class="text-small" style="margin-top: 10px;" ng-class="{ 'text-danger': (configureBackupForm.password.$dirty && configureBackup.password !== configureBackup.passwordRepeat) }">{{ 'backups.configureBackupStorage.encryptionPasswordRepeat' | tr }}</p>
|
||||
<input type="text" class="form-control" name="passwordRepeat" ng-model="configureBackup.passwordRepeat" ng-disabled="configureBackup.busy">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="" ng-click="configureBackup.advancedVisible = true" ng-hide="configureBackup.advancedVisible">{{ 'backups.configureBackupStorage.advancedSettings' | tr }}</a>
|
||||
<div uib-collapse="!configureBackup.advancedVisible">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="storageProviderProvider">Storage provider <sup><a ng-href="{{ config.webServerOrigin }}/documentation/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="configureBackup.provider" ng-options="a.value as a.name for a in storageProvider" ng-change=configureBackup.clearForm()></select>
|
||||
</div>
|
||||
|
||||
<!-- Noop -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'noop'">
|
||||
<p class="has-error">
|
||||
This option breaks the backup and restore functionality of Cloudron and should only be used for testing. Please make sure the server is completely backed up using alternate means.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Filesystem -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.backupFolder || !configureBackup.backupFolder }" ng-show="configureBackup.provider === 'filesystem'">
|
||||
<label class="control-label" for="inputConfigureBackupFolder">Local backup directory</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.backupFolder" id="inputConfigureBackupFolder" name="backupFolder" ng-disabled="configureBackup.busy" placeholder="Directory for backups" ng-required="configureBackup.provider === 'filesystem'">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'filesystem'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.externalDisk" id="inputConfigureExternalDisk">Backup directory is an external EXT4 Disk</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'filesystem'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.useHardlinks" id="inputConfigureUseHardlinks">Use hardlinks</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- S3/Minio/SOS/GCS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.endpoint }" ng-show="configureBackup.provider === 'minio' || configureBackup.provider === 's3-v4-compat'">
|
||||
<label class="control-label" for="inputConfigureBackupEndpoint">Endpoint</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.endpoint" id="inputConfigureBackupEndpoint" name="endpoint" ng-disabled="configureBackup.busy" placeholder="URL of Minio/S3 Compatible" ng-required="configureBackup.provider === 'minio' || configureBackup.provider === 's3-v4-compat'">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'minio' || configureBackup.provider === 's3-v4-compat'" >
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.acceptSelfSignedCerts" id="inputConfigureBackupSelfSigned">Accept Self-signed certificate</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.bucket }" ng-show="s3like(configureBackup.provider) || configureBackup.provider === 'gcs'">
|
||||
<label class="control-label" for="inputConfigureBackupBucket">Bucket name</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.bucket" id="inputConfigureBackupBucket" name="bucket" ng-disabled="configureBackup.busy" ng-required="s3like(configureBackup.provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.prefix }" ng-show="configureBackup.provider !== 'filesystem' && configureBackup.provider !== 'noop'">
|
||||
<label class="control-label" for="inputConfigureBackupPrefix">Prefix</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.prefix" id="inputConfigureBackupPrefix" name="prefix" ng-disabled="configureBackup.busy" placeholder="Prefix for backup file names">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 's3'">
|
||||
<label class="control-label" for="inputConfigureBackupS3Region">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupS3Region" ng-model="configureBackup.region" ng-options="a.value as a.name for a in s3Regions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 's3'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'digitalocean-spaces'">
|
||||
<label class="control-label" for="inputConfigureBackupDORegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupDORegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in doSpacesRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'digitalocean-spaces'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'exoscale-sos'">
|
||||
<label class="control-label" for="inputConfigureBackupExoscaleRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupExoscaleRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in exoscaleSosRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'exoscale-sos'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'wasabi'">
|
||||
<label class="control-label" for="inputConfigureBackupWasabiRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupWasabiRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in wasabiRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'wasabi'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'scaleway-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupScalewayRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupScalewayRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in scalewayRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'scaleway-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'linode-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupLinodeRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupLinodeRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in linodeRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'linode-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.accessKeyId }" ng-show="s3like(configureBackup.provider)">
|
||||
<label class="control-label" for="inputConfigureBackupAccessKeyId">Access key id</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.accessKeyId" id="inputConfigureBackupAccessKeyId" name="accessKeyId" ng-disabled="configureBackup.busy" ng-required="s3like(configureBackup.provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.secretAccessKey }" ng-show="s3like(configureBackup.provider)">
|
||||
<label class="control-label" for="inputConfigureBackupSecretAccessKey">Secret access key</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.secretAccessKey" id="inputConfigureBackupSecretAccessKey" name="secretAccessKey" ng-disabled="configureBackup.busy" ng-required="s3like(configureBackup.provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.gcsKeyInput }" ng-show="configureBackup.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="configureBackup.gcsKey.keyFileName" id="gcsKeyInput" name="cert" onclick="getElementById('gcsKeyFileInput').click();" style="cursor: pointer;" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'gcs'">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('gcsKeyFileInput').click();"></i>
|
||||
</span>
|
||||
<label class="control-label">{{ 'backups.configureBackupStorage.memoryLimit' | tr }}: <b>{{ configureBackup.memoryLimit | prettyByteSize:'800 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>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="configureBackup.provider !== 'noop'">
|
||||
<label class="control-label" for="storageFormat">Storage Format <sup><a ng-href="{{ config.webServerOrigin }}/documentation/backups/#backup-formats" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" id="storageFormat" ng-change="configureBackup.key = ''" ng-model="configureBackup.format" ng-options="a.value as a.name for a in formats"></select>
|
||||
<div class="form-group" ng-show="s3like(configureBackup.provider)">
|
||||
<label class="control-label">{{ 'backups.configureBackupStorage.uploadPartSize' | tr }}: <b>{{ configureBackup.uploadPartSize | prettyByteSize:'Default (50 MB)' }}</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>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="configureBackup.provider !== 'noop'">
|
||||
<label class="control-label" for="storageInterval">Backup Interval</label>
|
||||
<select class="form-control" id="storageInterval" ng-model="configureBackup.intervalSecs" ng-options="a.value as a.name for a in intervalTimes"></select>
|
||||
<div class="form-group" ng-show="configureBackup.format === 'rsync' && (s3like(configureBackup.provider) || configureBackup.provider === 'gcs')">
|
||||
<label class="control-label">{{ '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>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="configureBackup.provider !== 'noop'">
|
||||
<label class="control-label" for="storageRetention">Retention Time</label>
|
||||
<select class="form-control" id="storageRetention" ng-model="configureBackup.retentionSecs" ng-options="a.value as a.name for a in retentionTimes"></select>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.key }" ng-show="configureBackup.provider !== 'noop'">
|
||||
<label class="control-label" for="inputConfigureBackupKey">Encryption key (optional) <sup><a ng-href="{{ config.webServerOrigin }}/documentation/backups/#encryption" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<p class="small">Save this passphrase in a safe place. Backups cannot be decrypted without the passphrase</p>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.key" id="inputConfigureBackupKey" name="prefix" ng-disabled="configureBackup.busy" placeholder="Passphrase used to encrypt the backups">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<p class="text-info">
|
||||
If you change the location of backups, Cloudron will not delete backups stored in the previous location. They have to be removed manually.
|
||||
</p>
|
||||
<input class="ng-hide" type="submit" ng-disabled="configureBackupForm.$invalid"/>
|
||||
</fieldset>
|
||||
</div> <!-- advanced -->
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="configureBackupForm.$invalid || (configureBackup.password !== SECRET_PLACEHOLDER && configureBackup.password !== configureBackup.passwordRepeat)"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="configureBackup.submit()" ng-disabled="configureBackupForm.$invalid || configureBackup.busy"><i class="fa fa-circle-notch fa-spin" ng-show="configureBackup.busy"></i><span>Save</span></button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="configureBackup.submit()" ng-disabled="configureBackupForm.$invalid || configureBackup.busy || (configureBackup.password !== SECRET_PLACEHOLDER && configureBackup.password !== configureBackup.passwordRepeat)"><i class="fa fa-circle-notch fa-spin" ng-show="configureBackup.busy"></i><span> {{ 'main.dialog.save' | tr }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -172,13 +387,29 @@
|
||||
<div class="content">
|
||||
|
||||
<div class="text-left">
|
||||
<h1>Backups</h1>
|
||||
<h1>{{ 'backups.title' | tr }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'backups.location.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<p>{{ 'backups.location.description' | tr }}
|
||||
<span ng-show="manualBackupApps.length">
|
||||
{{ 'backups.location.disabledList' | tr }}
|
||||
<span ng-repeat="app in manualBackupApps">
|
||||
<a ng-href="/#/app/{{app.id}}/backups">{{app.label || app.fqdn}}</a><span ng-hide="$last">,</span>
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p ng-show="backupConfig.provider === 'noop'" class="text-danger" ng-bind-html="'backups.check.noop' | tr | markdown2html"></p>
|
||||
<p ng-show="backupConfig.provider === 'filesystem'" class="text-danger" ng-bind-html="'backups.check.sameDisk' | tr | markdown2html"></p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">Provider</span>
|
||||
<span class="text-muted">{{ 'backups.location.provider' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ prettyProviderName(backupConfig.provider) }}</span>
|
||||
@@ -186,82 +417,150 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">Location</span>
|
||||
<span class="text-muted">{{ 'backups.location.location' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<div class="col-xs-6 text-right no-wrap">
|
||||
<span ng-show="backupConfig.provider === 'filesystem'">{{ backupConfig.backupFolder }}</span>
|
||||
<span ng-show="mountlike(backupConfig.provider)">
|
||||
<i class="fa fa-circle" ng-style="{ color: backupConfig.mountStatus.state === 'active' ? '#27CE65' : '#d9534f' }" ng-show="backupConfig.mountStatus" uib-tooltip="{{ backupConfig.mountStatus.message }}"></i>
|
||||
<span ng-show="backupConfig.provider === 'filesystem' || backupConfig.provider === 'ext4' || backupConfig.provider === 'mountpoint'">{{ backupConfig.mountOptions.diskPath || backupConfig.mountPoint }}{{ (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
<span ng-show="backupConfig.provider === 'cifs' || backupConfig.provider === 'nfs' || backupConfig.provider === 'sshfs'">{{ backupConfig.mountOptions.host }}:{{ backupConfig.mountOptions.remoteDir }}{{ (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
</span>
|
||||
|
||||
<span ng-show="backupConfig.provider !== 's3' && backupConfig.provider !== 'minio' && (s3like(backupConfig.provider) || backupConfig.provider === 'gcs')">{{ backupConfig.bucket + (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
<span ng-show="backupConfig.provider === 's3'">{{ backupConfig.region + ' ' + backupConfig.bucket + (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
<span ng-show="backupConfig.provider === 'minio'">{{ backupConfig.endpoint + ' ' + backupConfig.bucket + (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row" ng-show="backupConfig.endpoint && backupConfig.provider !== 'minio'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">Storage Format</span>
|
||||
<span class="text-muted">{{ 'backups.location.endpoint' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ backupConfig.format }}</span>
|
||||
<span>{{ backupConfig.endpoint || backupConfig.region }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'backups.location.format' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ backupConfig.format }} <i class="fas fa-lock" ng-show="backupConfig.password" ></i></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-4">
|
||||
<span class="text-muted">Backup ID</span>
|
||||
</div>
|
||||
<div class="col-xs-8 text-right">
|
||||
<span ng-click-select ng-show="lastBackup">{{ lastBackup.id }}</span>
|
||||
<span ng-hide="lastBackup">No backups have been made yet</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">Last backup</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span uib-tooltip="{{ lastBackup.creationTime | prettyLongDate }}" ng-show="lastBackup">{{ lastBackup.creationTime | prettyDate }}</span>
|
||||
<span ng-hide="lastBackup">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<br/>
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<div ng-show="createBackup.busy" class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ createBackup.percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<p ng-show="createBackup.busy">{{ createBackup.message }}</p>
|
||||
<p ng-hide="createBackup.busy">
|
||||
<div class="has-error" ng-show="!createBackup.active">{{ createBackup.errorMessage }}</div>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="configureBackup.show()" ng-disabled="createBackup.busy">Configure</button>
|
||||
<button class="btn btn-outline btn-primary" ng-click="createBackup.startBackup()" ng-show="!createBackup.busy" style="margin-right: 10px">Backup now</button>
|
||||
<button class="btn btn-outline btn-danger" ng-click="createBackup.stopBackup()" ng-show="createBackup.busy" style="margin-right: 10px">Stop Backup</button>
|
||||
<div class="col-md-12">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-show="user.isAtLeastOwner" ng-click="configureBackup.show()">{{ 'backups.location.configure' | tr }}</button>
|
||||
<button class="btn btn-outline btn-default pull-right" ng-show="user.isAtLeastOwner && mountlike(backupConfig.provider)" ng-disabled="remount.busy" ng-click="remount.submit()"><i class="fa fa-circle-notch fa-spin" ng-show="remount.busy"></i> {{ 'backups.location.remount' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>Logs</h3>
|
||||
<h3>{{ 'backups.schedule.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<p>{{ 'backups.schedule.description' | tr }}</p>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'backups.schedule.schedule' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ prettyBackupSchedule(backupConfig.schedulePattern) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'backups.schedule.retentionPolicy' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ prettyBackupRetentionPolicy(backupConfig.retentionPolicy) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-show="user.isAtLeastOwner" ng-click="configureScheduleAndRetention.show()">{{ 'backups.schedule.configure' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'backups.listing.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-show="!backups.length">{{ 'backups.listing.noBackups' | tr }}</p>
|
||||
|
||||
<table class="table table-hover" style="margin: 0;" ng-hide="!backups.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'backups.listing.version' | tr }}</th>
|
||||
<th>{{ 'main.table.date' | tr }}</th>
|
||||
<th>{{ 'backups.listing.contents' | tr }}</th>
|
||||
<th class="text-right" width="180px">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="backup in backups">
|
||||
<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 }}</span></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>
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-default" ng-click="downloadConfig(backup)" uib-tooltip="{{ 'backups.listing.tooltipDownloadBackupConfig' | tr }}"><i class="fas fa-file-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row" ng-show="createBackup.busy">
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<div class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ createBackup.percent }}%"></div>
|
||||
</div>
|
||||
<p>{{ createBackup.message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="!createBackup.busy && !createBackup.active && createBackup.errorMessage">
|
||||
<div class="col-md-12">
|
||||
<p class="has-error">{{ createBackup.errorMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-default" ng-click="createBackup.cleanupBackups()" ng-show="!createBackup.busy" style="margin-right: 5px">{{ 'backups.listing.cleanupBackups' | tr }}</button>
|
||||
<button class="btn btn-outline btn-primary" ng-click="createBackup.startBackup()" ng-show="!createBackup.busy">{{ 'backups.listing.backupNow' | tr }}</button>
|
||||
<button class="btn btn-outline btn-danger" ng-click="createBackup.stopTask()" ng-show="createBackup.busy">{{ 'backups.listing.stopTask' | tr:{ taskType: createBackup.taskType } }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'backups.logs.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>
|
||||
Please be careful when uploading these logs to a public server since they may contain sensitive information.
|
||||
</p>
|
||||
<a class="btn btn-primary pull-right" ng-href="/logs.html?taskId={{createBackup.taskId}}" ng-disabled="!createBackup.taskId" target="_blank">Show Logs</a>
|
||||
<p>{{ 'backups.logs.description' | tr }}</p>
|
||||
<a class="btn btn-primary pull-right" ng-href="/logs.html?taskId={{createBackup.taskId}}" ng-disabled="!createBackup.taskId" target="_blank">{{ 'backups.logs.showLogs' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+435
-53
@@ -1,16 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular:false */
|
||||
/* global $:false */
|
||||
/* global $, angular, TASK_TYPES, SECRET_PLACEHOLDER */
|
||||
|
||||
angular.module('Application').controller('BackupsController', ['$scope', '$location', '$rootScope', '$timeout', 'Client', function ($scope, $location, $rootScope, $timeout, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.SECRET_PLACEHOLDER = SECRET_PLACEHOLDER;
|
||||
$scope.MIN_MEMORY_LIMIT = 800 * 1024 * 1024;
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.memory = null; // { memory, swap }
|
||||
|
||||
$scope.manualBackupApps = [];
|
||||
|
||||
$scope.backupConfig = {};
|
||||
$scope.lastBackup = null;
|
||||
$scope.backups = [];
|
||||
|
||||
// List is from http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
|
||||
@@ -22,6 +26,8 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
{ name: 'Asia Pacific (Sydney)', value: 'ap-southeast-2' },
|
||||
{ name: 'Asia Pacific (Tokyo)', value: 'ap-northeast-1' },
|
||||
{ name: 'Canada (Central)', value: 'ca-central-1' },
|
||||
{ name: 'China (Beijing)', value: 'cn-north-1' },
|
||||
{ name: 'China (Ningxia)', value: 'cn-northwest-1' },
|
||||
{ name: 'EU (Frankfurt)', value: 'eu-central-1' },
|
||||
{ name: 'EU (Ireland)', value: 'eu-west-1' },
|
||||
{ name: 'EU (London)', value: 'eu-west-2' },
|
||||
@@ -36,7 +42,8 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
|
||||
$scope.wasabiRegions = [
|
||||
{ name: 'EU Central 1', value: 'https://s3.eu-central-1.wasabisys.com' },
|
||||
{ name: 'US East 1', value: 'https://s3.wasabisys.com' },
|
||||
{ name: 'US East 1', value: 'https://s3.us-east-1.wasabisys.com' },
|
||||
{ name: 'US East 2', value: 'https://s3.us-east-2.wasabisys.com ' },
|
||||
{ name: 'US West 1', value: 'https://s3.us-west-1.wasabisys.com' }
|
||||
];
|
||||
|
||||
@@ -45,6 +52,7 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
{ name: 'FRA1', value: 'https://fra1.digitaloceanspaces.com' },
|
||||
{ name: 'NYC3', value: 'https://nyc3.digitaloceanspaces.com' },
|
||||
{ name: 'SFO2', value: 'https://sfo2.digitaloceanspaces.com' },
|
||||
{ name: 'SFO3', value: 'https://sfo3.digitaloceanspaces.com' },
|
||||
{ name: 'SGP1', value: 'https://sgp1.digitaloceanspaces.com' }
|
||||
];
|
||||
|
||||
@@ -64,37 +72,92 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.linodeRegions = [
|
||||
{ name: 'Newark', value: 'us-east-1.linodeobjects.com', region: 'us-east-1' }, // default
|
||||
{ name: 'Frankfurt', value: 'eu-central-1.linodeobjects.com', region: 'us-east-1' },
|
||||
{ name: 'Singapore', value: 'ap-south-1.linodeobjects.com', region: 'us-east-1' }
|
||||
];
|
||||
|
||||
// note: ovh also has a storage endpoint but that only supports path style access
|
||||
$scope.ovhRegions = [
|
||||
{ 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' },
|
||||
];
|
||||
|
||||
// https://devops.ionos.com/api/s3/
|
||||
$scope.ionosRegions = [
|
||||
{ name: 'DE', value: 'https://s3-de-central.profitbricks.com', region: 's3-de-central' }, // default
|
||||
];
|
||||
|
||||
// this is not used anywhere because upcloud needs endpoint URL. we detect region from the URL
|
||||
$scope.upcloudRegions = [
|
||||
{ name: 'AU-SYD1 (Australia)', value: 'https://au-syd1.upcloudobjects.com', region: 'au-syd1' }, // default
|
||||
{ name: 'DE-FRA1 (Germany)', value: 'https://de-fra1.upcloudobjects.com', region: 'de-fra1' },
|
||||
{ name: 'ES-MAD1 (Spain)', value: 'https://es-mad1.upcloudobjects.com', region: 'es-mad1' },
|
||||
{ name: 'FI-HEL2 (Finland)', value: 'https://fi-hel2.upcloudobjects.com', region: 'fi-hel2' },
|
||||
{ name: 'NL-AMS1 (Netherlands)', value: 'https://nl-ams1.upcloudobjects.com', region: 'nl-ams1' },
|
||||
{ name: 'PL-WAW1 (Poland)', value: 'https://pl-waw1.upcloudobjects.com', region: 'pl-waw1' },
|
||||
{ name: 'SG-SIN1 (Singapore)', value: 'https://sg-sin1.upcloudobjects.com', region: 'sg-sin1' },
|
||||
{ name: 'UK-LON1 (United Kingdom)', value: 'https://uk-lon1.upcloudobjects.com', region: 'uk-lon1' },
|
||||
{ name: 'US-CHI1 (USA)', value: 'https://us-chi1.upcloudobjects.com', region: 'us-chi1' },
|
||||
{ name: 'US-NYC1 (USA)', value: 'https://us-nyc1.upcloudobjects.com', region: 'us-nyc1' },
|
||||
{ name: 'US-SJO1 (USA)', value: 'https://us-sjo1.upcloudobjects.com', region: 'us-sjo1' },
|
||||
];
|
||||
|
||||
$scope.vultrRegions = [
|
||||
{ name: 'New Jersey', value: 'https://ewr1.vultrobjects.com', region: 'us-east-1' }, // default
|
||||
];
|
||||
|
||||
$scope.storageProvider = [
|
||||
{ name: 'Amazon S3', value: 's3' },
|
||||
{ name: 'Backblaze B2 (S3 API)', value: 'backblaze-b2' },
|
||||
{ name: 'CIFS Mount', value: 'cifs' },
|
||||
{ name: 'DigitalOcean Spaces', value: 'digitalocean-spaces' },
|
||||
{ name: 'EXT4 Disk', value: 'ext4' },
|
||||
{ name: 'Exoscale SOS', value: 'exoscale-sos' },
|
||||
{ name: 'Filesystem', value: 'filesystem' },
|
||||
{ name: 'Filesystem (Mountpoint)', value: 'mountpoint' }, // legacy
|
||||
{ name: 'Google Cloud Storage', value: 'gcs' },
|
||||
{ name: 'IONOS (Profitbricks)', value: 'ionos-objectstorage' },
|
||||
{ name: 'Linode Object Storage', value: 'linode-objectstorage' },
|
||||
{ name: 'Minio', value: 'minio' },
|
||||
{ name: 'Scaleway Object Storage', value: 'scaleway-objectstorage' },
|
||||
{ name: 'No-op (Only for testing)', value: 'noop' },
|
||||
{ name: 'NFS Mount', value: 'nfs' },
|
||||
{ name: 'OVH Object Storage', value: 'ovh-objectstorage' },
|
||||
{ name: 'S3 API Compatible (v4)', value: 's3-v4-compat' },
|
||||
{ name: 'Wasabi', value: 'wasabi' }
|
||||
{ name: 'Scaleway Object Storage', value: 'scaleway-objectstorage' },
|
||||
{ name: 'SSHFS Mount', value: 'sshfs' },
|
||||
{ name: 'UpCloud Object Storage', value: 'upcloud-objectstorage' },
|
||||
{ name: 'Vultr Object Storage', value: 'vultr-objectstorage' },
|
||||
{ name: 'Wasabi', value: 'wasabi' },
|
||||
{ name: 'No-op (Only for testing)', value: 'noop' }
|
||||
];
|
||||
|
||||
$scope.retentionTimes = [
|
||||
{ name: '2 days', value: 2 * 24 * 60 * 60 },
|
||||
{ name: '1 week', value: 7 * 24 * 60 * 60},
|
||||
{ name: '1 month', value: 30 * 24 * 60 * 60},
|
||||
{ name: 'Forever', value: -1 }
|
||||
$scope.retentionPolicies = [
|
||||
{ name: '2 days', value: { keepWithinSecs: 2 * 24 * 60 * 60 }},
|
||||
{ name: '1 week', value: { keepWithinSecs: 7 * 24 * 60 * 60 }}, // default
|
||||
{ name: '1 month', value: { keepWithinSecs: 30 * 24 * 60 * 60 }},
|
||||
{ name: '2 daily, 4 weekly', value: { keepDaily: 2, keepWeekly: 4 }},
|
||||
{ name: '3 daily, 4 weekly, 6 monthly', value: { keepDaily: 3, keepWeekly: 4, keepMonthly: 6 }},
|
||||
{ name: '7 daily, 4 weekly, 12 monthly', value: { keepDaily: 7, keepWeekly: 4, keepMonthly: 12 }},
|
||||
{ name: 'Forever', value: { keepWithinSecs: -1 }}
|
||||
];
|
||||
|
||||
$scope.intervalTimes = [
|
||||
{ name: 'Every 6 hours', value: 6 * 60 * 60 },
|
||||
{ name: 'Every 12 hours', value: 12 * 60 * 60 },
|
||||
{ name: 'Every day', value: 24 * 60 * 60 },
|
||||
{ name: 'Every 3 days', value: 3 * 24 * 60 * 60 },
|
||||
{ name: 'Every week', value: 7 * 24 * 60 * 60 },
|
||||
// values correspond to cron days
|
||||
$scope.cronDays = [
|
||||
{ name: 'Sunday', value: 0 },
|
||||
{ name: 'Monday', value: 1 },
|
||||
{ name: 'Tuesday', value: 2 },
|
||||
{ name: 'Wednesday', value: 3 },
|
||||
{ name: 'Thursday', value: 4 },
|
||||
{ name: 'Friday', value: 5 },
|
||||
{ name: 'Saturday', value: 6 },
|
||||
];
|
||||
|
||||
// generates 24h time sets (instead of american 12h) to avoid having to translate everything to locales eg. 12:00
|
||||
$scope.cronHours = Array.from({ length: 24 }).map(function (v, i) { return { name: (i < 10 ? '0' : '') + i + ':00', value: i }; });
|
||||
|
||||
$scope.formats = [
|
||||
{ name: 'Tarball (zipped)', value: 'tgz' },
|
||||
{ name: 'rsync', value: 'rsync' }
|
||||
@@ -107,15 +170,63 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
}
|
||||
};
|
||||
|
||||
$scope.prettyBackupSchedule = function (pattern) {
|
||||
if (!pattern) return '';
|
||||
var tmp = pattern.split(' ');
|
||||
var hours = tmp[2].split(','), days = tmp[5].split(',');
|
||||
var prettyDay;
|
||||
if (days.length === 7 || days[0] === '*') {
|
||||
prettyDay = 'Everyday';
|
||||
} else {
|
||||
prettyDay = days.map(function (day) { return $scope.cronDays[parseInt(day, 10)].name.substr(0, 3); }).join(',');
|
||||
}
|
||||
|
||||
var prettyHour = hours.map(function (hour) { return $scope.cronHours[parseInt(hour, 10)].name; }).join(',');
|
||||
|
||||
return prettyDay + ' at ' + prettyHour;
|
||||
};
|
||||
|
||||
$scope.prettyBackupRetentionPolicy = function (retentionPolicy) {
|
||||
var tmp = $scope.retentionPolicies.find(function (p) { return angular.equals(p.value, retentionPolicy); });
|
||||
return tmp ? tmp.name : '';
|
||||
};
|
||||
|
||||
$scope.remount = {
|
||||
busy: false,
|
||||
error: null,
|
||||
|
||||
submit: function () {
|
||||
if (!$scope.mountlike($scope.backupConfig.provider)) return;
|
||||
|
||||
$scope.remount.busy = true;
|
||||
$scope.remount.error = null;
|
||||
|
||||
Client.remountBackupStorage(function (error) {
|
||||
if (error) {
|
||||
console.error('Failed to remount backup storage.', error);
|
||||
$scope.remount.error = error.message;
|
||||
}
|
||||
|
||||
// give the backend some time
|
||||
$timeout(function () {
|
||||
$scope.remount.busy = false;
|
||||
getBackupConfig();
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.createBackup = {
|
||||
busy: false,
|
||||
percent: 0,
|
||||
message: '',
|
||||
errorMessage: '',
|
||||
taskId: '',
|
||||
taskType: TASK_TYPES.TASK_BACKUP,
|
||||
|
||||
checkStatus: function () {
|
||||
Client.getLatestTaskByType('backup', function (error, task) {
|
||||
// TODO support both task types TASK_BACKUP and TASK_CLEAN_BACKUPS
|
||||
Client.getLatestTaskByType($scope.createBackup.taskType, function (error, task) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!task) return;
|
||||
@@ -150,6 +261,7 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.createBackup.percent = 0;
|
||||
$scope.createBackup.message = '';
|
||||
$scope.createBackup.errorMessage = '';
|
||||
$scope.createBackup.taskType = TASK_TYPES.TASK_BACKUP;
|
||||
|
||||
Client.startBackup(function (error, taskId) {
|
||||
if (error) {
|
||||
@@ -173,7 +285,28 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
});
|
||||
},
|
||||
|
||||
stopBackup: function () {
|
||||
cleanupBackups: function () {
|
||||
$('#cleanupBackupsModal').modal('show');
|
||||
},
|
||||
|
||||
startCleanup: function () {
|
||||
$scope.createBackup.busy = true;
|
||||
$scope.createBackup.percent = 0;
|
||||
$scope.createBackup.message = '';
|
||||
$scope.createBackup.errorMessage = '';
|
||||
$scope.createBackup.taskType = TASK_TYPES.TASK_CLEAN_BACKUPS;
|
||||
|
||||
$('#cleanupBackupsModal').modal('hide');
|
||||
|
||||
Client.cleanupBackups(function (error, taskId) {
|
||||
if (error) console.error(error);
|
||||
|
||||
$scope.createBackup.taskId = taskId;
|
||||
$scope.createBackup.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
stopTask: function () {
|
||||
Client.stopTask($scope.createBackup.taskId, function (error) {
|
||||
if (error) {
|
||||
if (error.statusCode === 409) {
|
||||
@@ -191,11 +324,126 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
}
|
||||
};
|
||||
|
||||
$scope.listBackups = {
|
||||
};
|
||||
|
||||
$scope.s3like = function (provider) {
|
||||
return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat'
|
||||
|| provider === 'exoscale-sos' || provider === 'digitalocean-spaces'
|
||||
|| provider === 'scaleway-objectstorage' || provider === 'wasabi'
|
||||
|| provider === 'linode-objectstorage';
|
||||
|| provider === 'scaleway-objectstorage' || provider === 'wasabi' || provider === 'backblaze-b2'
|
||||
|| provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'ionos-objectstorage'
|
||||
|| provider === 'vultr-objectstorage' || provider === 'upcloud-objectstorage';
|
||||
};
|
||||
|
||||
$scope.mountlike = function (provider) {
|
||||
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4';
|
||||
};
|
||||
|
||||
// https://stackoverflow.com/questions/3665115/how-to-create-a-file-in-memory-for-user-to-download-but-not-through-server#18197341
|
||||
function download(filename, text) {
|
||||
var element = document.createElement('a');
|
||||
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
|
||||
element.setAttribute('download', filename);
|
||||
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
|
||||
element.click();
|
||||
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
$scope.downloadConfig = function (backup) {
|
||||
// secrets and tokens already come with placeholder characters we remove them
|
||||
var tmp = {
|
||||
backupId: backup.id,
|
||||
encrypted: !!$scope.backupConfig.password // we add this just to help the import UI
|
||||
};
|
||||
|
||||
Object.keys($scope.backupConfig).forEach(function (k) {
|
||||
if ($scope.backupConfig[k] !== SECRET_PLACEHOLDER) tmp[k] = $scope.backupConfig[k];
|
||||
});
|
||||
|
||||
var filename = 'cloudron-backup-config-' + (new Date).toISOString().replace(/:|T/g,'-').replace(/\..*/,'') + '.json';
|
||||
download(filename, JSON.stringify(tmp));
|
||||
};
|
||||
|
||||
$scope.backupDetails = {
|
||||
backup: null,
|
||||
|
||||
show: function (backup) {
|
||||
$scope.backupDetails.backup = backup;
|
||||
$('#backupDetailsModal').modal('show');
|
||||
}
|
||||
};
|
||||
|
||||
$scope.configureScheduleAndRetention = {
|
||||
busy: false,
|
||||
error: {},
|
||||
|
||||
retentionPolicy: $scope.retentionPolicies[0],
|
||||
days: [],
|
||||
hours: [],
|
||||
|
||||
show: function () {
|
||||
$scope.configureScheduleAndRetention.error = {};
|
||||
$scope.configureScheduleAndRetention.busy = false;
|
||||
|
||||
var selectedPolicy = $scope.retentionPolicies.find(function (x) { return angular.equals(x.value, $scope.backupConfig.retentionPolicy); });
|
||||
if (!selectedPolicy) selectedPolicy = $scope.retentionPolicies[0];
|
||||
|
||||
$scope.configureScheduleAndRetention.retentionPolicy = selectedPolicy.value;
|
||||
|
||||
var tmp = $scope.backupConfig.schedulePattern.split(' ');
|
||||
var hours = tmp[2].split(','), days = tmp[5].split(',');
|
||||
if (days[0] === '*') {
|
||||
$scope.configureScheduleAndRetention.days = angular.copy($scope.cronDays, []);
|
||||
} else {
|
||||
$scope.configureScheduleAndRetention.days = days.map(function (day) { return $scope.cronDays[parseInt(day, 10)]; });
|
||||
}
|
||||
$scope.configureScheduleAndRetention.hours = hours.map(function (hour) { return $scope.cronHours[parseInt(hour, 10)]; });
|
||||
|
||||
$('#configureScheduleAndRetentionModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.configureScheduleAndRetention.error = {};
|
||||
$scope.configureScheduleAndRetention.busy = true;
|
||||
|
||||
// start with the full backupConfig since the api requires all fields
|
||||
var backupConfig = $scope.backupConfig;
|
||||
backupConfig.retentionPolicy = $scope.configureScheduleAndRetention.retentionPolicy;
|
||||
|
||||
var daysPattern;
|
||||
if ($scope.configureScheduleAndRetention.days.length === 7) daysPattern = '*';
|
||||
else daysPattern = $scope.configureScheduleAndRetention.days.map(function (d) { return d.value; });
|
||||
|
||||
var hoursPattern;
|
||||
if ($scope.configureScheduleAndRetention.hours.length === 24) hoursPattern = '*';
|
||||
else hoursPattern = $scope.configureScheduleAndRetention.hours.map(function (d) { return d.value; });
|
||||
|
||||
backupConfig.schedulePattern ='00 00 ' + hoursPattern + ' * * ' + daysPattern;
|
||||
|
||||
Client.setBackupConfig(backupConfig, function (error) {
|
||||
$scope.configureScheduleAndRetention.busy = false;
|
||||
|
||||
if (error) {
|
||||
if (error.statusCode === 424) {
|
||||
$scope.configureScheduleAndRetention.error.generic = error.message;
|
||||
} else if (error.statusCode === 400) {
|
||||
$scope.configureScheduleAndRetention.error.generic = error.message;
|
||||
} else {
|
||||
console.error('Unable to change schedule or retention.', error);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$('#configureScheduleAndRetentionModal').modal('hide');
|
||||
|
||||
getBackupConfig();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.configureBackup = {
|
||||
@@ -211,15 +459,36 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
region: '',
|
||||
endpoint: '',
|
||||
backupFolder: '',
|
||||
retentionSecs: 7 * 24 * 60 * 60,
|
||||
intervalSecs: 24 * 60 * 60,
|
||||
mountPoint: '',
|
||||
acceptSelfSignedCerts: false,
|
||||
useHardlinks: true,
|
||||
externalDisk: false,
|
||||
chown: true,
|
||||
format: 'tgz',
|
||||
key: '',
|
||||
password: '',
|
||||
passwordRepeat: '',
|
||||
advancedVisible: false,
|
||||
|
||||
clearForm: function () {
|
||||
memoryTicks: [],
|
||||
memoryLimit: $scope.MIN_MEMORY_LIMIT,
|
||||
uploadPartSizeTicks: [],
|
||||
uploadPartSize: 50 * 1024 * 1024,
|
||||
copyConcurrency: '',
|
||||
downloadConcurrency: '',
|
||||
syncConcurrency: '', // sort of similar to upload
|
||||
|
||||
mountOptions: {
|
||||
host: '',
|
||||
remoteDir: '',
|
||||
username: '',
|
||||
password: '',
|
||||
diskPath: '',
|
||||
seal: false,
|
||||
user: '',
|
||||
port: 22,
|
||||
privateKey: ''
|
||||
},
|
||||
|
||||
clearProviderFields: function () {
|
||||
$scope.configureBackup.bucket = '';
|
||||
$scope.configureBackup.prefix = '';
|
||||
$scope.configureBackup.accessKeyId = '';
|
||||
@@ -229,19 +498,27 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.configureBackup.endpoint = '';
|
||||
$scope.configureBackup.region = '';
|
||||
$scope.configureBackup.backupFolder = '';
|
||||
$scope.configureBackup.retentionSecs = 7 * 24 * 60 * 60;
|
||||
$scope.configureBackup.intervalSecs = 24 * 60 * 60;
|
||||
$scope.configureBackup.format = 'tgz';
|
||||
$scope.configureBackup.mountPoint = '';
|
||||
$scope.configureBackup.acceptSelfSignedCerts = false;
|
||||
$scope.configureBackup.useHardlinks = true;
|
||||
$scope.configureBackup.externalDisk = false;
|
||||
$scope.configureBackup.key = '';
|
||||
$scope.configureBackup.chown = true;
|
||||
$scope.configureBackup.memoryLimit = $scope.MIN_MEMORY_LIMIT;
|
||||
|
||||
// scaleway only supports 1000 parts per object (https://www.scaleway.com/en/docs/s3-multipart-upload/)
|
||||
$scope.configureBackup.uploadPartSize = $scope.configureBackup.provider === 'scaleway-objectstorage' ? 100 * 1024 * 1024 : 10 * 1024 * 1024;
|
||||
$scope.configureBackup.downloadConcurrency = $scope.configureBackup.provider === 's3' ? 30 : 10;
|
||||
$scope.configureBackup.syncConcurrency = $scope.configureBackup.provider === 's3' ? 20 : 10;
|
||||
$scope.configureBackup.copyConcurrency = $scope.configureBackup.provider === 's3' ? 500 : 10;
|
||||
|
||||
$scope.configureBackup.mountOptions = { host: '', remoteDir: '', username: '', password: '', diskPath: '', seal: false, user: '', port: 22, privateKey: '' };
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.configureBackup.error = {};
|
||||
$scope.configureBackup.busy = false;
|
||||
|
||||
$scope.configureBackup.advancedVisible = false;
|
||||
|
||||
$scope.configureBackup.provider = $scope.backupConfig.provider;
|
||||
$scope.configureBackup.bucket = $scope.backupConfig.bucket;
|
||||
$scope.configureBackup.prefix = $scope.backupConfig.prefix;
|
||||
@@ -257,14 +534,45 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
});
|
||||
}
|
||||
$scope.configureBackup.endpoint = $scope.backupConfig.endpoint;
|
||||
$scope.configureBackup.key = $scope.backupConfig.key;
|
||||
$scope.configureBackup.password = $scope.backupConfig.password || '';
|
||||
$scope.configureBackup.passwordRepeat = '';
|
||||
$scope.configureBackup.backupFolder = $scope.backupConfig.backupFolder;
|
||||
$scope.configureBackup.retentionSecs = $scope.backupConfig.retentionSecs;
|
||||
$scope.configureBackup.intervalSecs = $scope.backupConfig.intervalSecs;
|
||||
$scope.configureBackup.mountPoint = $scope.backupConfig.mountPoint;
|
||||
$scope.configureBackup.format = $scope.backupConfig.format;
|
||||
$scope.configureBackup.acceptSelfSignedCerts = !!$scope.backupConfig.acceptSelfSignedCerts;
|
||||
$scope.configureBackup.useHardlinks = !$scope.backupConfig.noHardlinks;
|
||||
$scope.configureBackup.externalDisk = !!$scope.backupConfig.externalDisk;
|
||||
$scope.configureBackup.chown = $scope.backupConfig.chown;
|
||||
|
||||
$scope.configureBackup.memoryLimit = $scope.backupConfig.memoryLimit;
|
||||
|
||||
$scope.configureBackup.uploadPartSize = $scope.backupConfig.uploadPartSize || ($scope.configureBackup.provider === 'scaleway-objectstorage' ? 100 * 1024 * 1024 : 10 * 1024 * 1024);
|
||||
$scope.configureBackup.downloadConcurrency = $scope.backupConfig.downloadConcurrency || ($scope.backupConfig.provider === 's3' ? 30 : 10);
|
||||
$scope.configureBackup.syncConcurrency = $scope.backupConfig.syncConcurrency || ($scope.backupConfig.provider === 's3' ? 20 : 10);
|
||||
$scope.configureBackup.copyConcurrency = $scope.backupConfig.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 || '',
|
||||
remoteDir: mountOptions.remoteDir || '',
|
||||
username: mountOptions.username || '',
|
||||
password: mountOptions.password || '',
|
||||
diskPath: mountOptions.diskPath || '',
|
||||
seal: mountOptions.seal,
|
||||
user: mountOptions.user || '',
|
||||
port: mountOptions.port || 22,
|
||||
privateKey: mountOptions.privateKey || ''
|
||||
};
|
||||
|
||||
$('#configureBackupModal').modal('show');
|
||||
},
|
||||
@@ -275,11 +583,13 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
|
||||
var backupConfig = {
|
||||
provider: $scope.configureBackup.provider,
|
||||
key: $scope.configureBackup.key,
|
||||
retentionSecs: $scope.configureBackup.retentionSecs,
|
||||
intervalSecs: $scope.configureBackup.intervalSecs,
|
||||
format: $scope.configureBackup.format
|
||||
format: $scope.configureBackup.format,
|
||||
memoryLimit: $scope.configureBackup.memoryLimit,
|
||||
// required for api call to provide all fields
|
||||
schedulePattern: $scope.backupConfig.schedulePattern,
|
||||
retentionPolicy: $scope.backupConfig.retentionPolicy
|
||||
};
|
||||
if ($scope.configureBackup.password) backupConfig.password = $scope.configureBackup.password;
|
||||
|
||||
// only set provider specific fields, this will clear them in the db
|
||||
if ($scope.s3like(backupConfig.provider)) {
|
||||
@@ -294,13 +604,14 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
if ($scope.configureBackup.region) backupConfig.region = $scope.configureBackup.region;
|
||||
delete backupConfig.endpoint;
|
||||
} else if (backupConfig.provider === 'minio' || backupConfig.provider === 's3-v4-compat') {
|
||||
backupConfig.region = 'us-east-1';
|
||||
backupConfig.region = $scope.configureBackup.region || 'us-east-1';
|
||||
backupConfig.acceptSelfSignedCerts = $scope.configureBackup.acceptSelfSignedCerts;
|
||||
backupConfig.s3ForcePathStyle = true; // might want to expose this in the UI
|
||||
} else if (backupConfig.provider === 'exoscale-sos') {
|
||||
backupConfig.region = 'us-east-1';
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'wasabi') {
|
||||
backupConfig.region = 'us-east-1';
|
||||
backupConfig.region = $scope.wasabiRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'scaleway-objectstorage') {
|
||||
backupConfig.region = $scope.scalewayRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
|
||||
@@ -308,6 +619,19 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
} else if (backupConfig.provider === 'linode-objectstorage') {
|
||||
backupConfig.region = $scope.linodeRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'ovh-objectstorage') {
|
||||
backupConfig.region = $scope.ovhRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'ionos-objectstorage') {
|
||||
backupConfig.region = $scope.ionosRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'vultr-objectstorage') {
|
||||
backupConfig.region = $scope.vultrRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'upcloud-objectstorage') { // the UI sets region and endpoint
|
||||
var m = /^.*\.(.*)\.upcloudobjects.com$/.exec(backupConfig.endpoint);
|
||||
backupConfig.region = m ? m[1] : 'us-east-1'; // let it fail in validation phase if m is not valid
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'digitalocean-spaces') {
|
||||
backupConfig.region = 'us-east-1';
|
||||
}
|
||||
@@ -331,10 +655,42 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.configureBackup.busy = false;
|
||||
return;
|
||||
}
|
||||
} else if ($scope.mountlike(backupConfig.provider)) {
|
||||
backupConfig.prefix = $scope.configureBackup.prefix;
|
||||
backupConfig.noHardlinks = !$scope.configureBackup.useHardlinks;
|
||||
backupConfig.mountOptions = {};
|
||||
|
||||
if (backupConfig.provider === 'cifs' || backupConfig.provider === 'sshfs' || backupConfig.provider === 'nfs') {
|
||||
backupConfig.mountOptions.host = $scope.configureBackup.mountOptions.host;
|
||||
backupConfig.mountOptions.remoteDir = $scope.configureBackup.mountOptions.remoteDir;
|
||||
|
||||
if (backupConfig.provider === 'cifs') {
|
||||
backupConfig.mountOptions.username = $scope.configureBackup.mountOptions.username;
|
||||
backupConfig.mountOptions.password = $scope.configureBackup.mountOptions.password;
|
||||
backupConfig.mountOptions.seal = $scope.configureBackup.mountOptions.seal;
|
||||
} else if (backupConfig.provider === 'sshfs') {
|
||||
backupConfig.mountOptions.user = $scope.configureBackup.mountOptions.user;
|
||||
backupConfig.mountOptions.port = $scope.configureBackup.mountOptions.port;
|
||||
backupConfig.mountOptions.privateKey = $scope.configureBackup.mountOptions.privateKey;
|
||||
}
|
||||
} else if (backupConfig.provider === 'ext4') {
|
||||
backupConfig.mountOptions.diskPath = $scope.configureBackup.mountOptions.diskPath;
|
||||
} else if (backupConfig.provider === 'mountpoint') {
|
||||
backupConfig.mountPoint = $scope.configureBackup.mountPoint;
|
||||
backupConfig.chown = $scope.configureBackup.chown;
|
||||
backupConfig.preserveAttributes = true;
|
||||
}
|
||||
} else if (backupConfig.provider === 'filesystem') {
|
||||
backupConfig.backupFolder = $scope.configureBackup.backupFolder;
|
||||
backupConfig.noHardlinks = !$scope.configureBackup.useHardlinks;
|
||||
backupConfig.externalDisk = $scope.configureBackup.externalDisk;
|
||||
}
|
||||
|
||||
backupConfig.uploadPartSize = $scope.configureBackup.uploadPartSize;
|
||||
|
||||
if (backupConfig.format === 'rsync') {
|
||||
backupConfig.downloadConcurrency = $scope.configureBackup.downloadConcurrency;
|
||||
backupConfig.syncConcurrency = $scope.configureBackup.syncConcurrency;
|
||||
backupConfig.copyConcurrency = $scope.configureBackup.copyConcurrency;
|
||||
}
|
||||
|
||||
Client.setBackupConfig(backupConfig, function (error) {
|
||||
@@ -375,7 +731,10 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
} else if (error.statusCode === 400) {
|
||||
$scope.configureBackup.error.generic = error.message;
|
||||
|
||||
if ($scope.configureBackup.provider === 'filesystem') {
|
||||
if (error.message.indexOf('password') !== -1) {
|
||||
$scope.configureBackup.error.password = true;
|
||||
$scope.configureBackupForm.password.$setPristine();
|
||||
} else if ($scope.configureBackup.provider === 'filesystem') {
|
||||
$scope.configureBackup.error.backupFolder = true;
|
||||
}
|
||||
} else {
|
||||
@@ -398,12 +757,27 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.backups = backups;
|
||||
$scope.backups = $scope.backups.slice(0, 20); // only show 20 since we don't have pagination
|
||||
|
||||
if ($scope.backups.length > 0) {
|
||||
$scope.lastBackup = backups[0];
|
||||
} else {
|
||||
$scope.lastBackup = null;
|
||||
}
|
||||
// add contents property
|
||||
var appsById = {}, appsByFqdn = {};
|
||||
Client.getInstalledApps().forEach(function (app) {
|
||||
appsById[app.id] = app;
|
||||
appsByFqdn[app.fqdn] = app;
|
||||
});
|
||||
|
||||
$scope.backups.forEach(function (backup) {
|
||||
backup.contents = [];
|
||||
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]]);
|
||||
} else {
|
||||
if (appsById[match[1]]) backup.contents.push(appsById[match[1]]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -416,11 +790,19 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
}
|
||||
|
||||
Client.onReady(function () {
|
||||
fetchBackups();
|
||||
getBackupConfig();
|
||||
Client.memory(function (error, memory) {
|
||||
if (error) console.error(error);
|
||||
|
||||
// show backup status
|
||||
$scope.createBackup.checkStatus();
|
||||
$scope.memory = memory;
|
||||
|
||||
fetchBackups();
|
||||
getBackupConfig();
|
||||
|
||||
$scope.manualBackupApps = Client.getInstalledApps().filter(function (app) { return !app.enableBackup; });
|
||||
|
||||
// show backup status
|
||||
$scope.createBackup.checkStatus();
|
||||
});
|
||||
});
|
||||
|
||||
function readFileLocally(obj, file, fileName) {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<!-- 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<div class="text-left">
|
||||
<h1>{{ 'branding.title' | tr }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form role="form" name="aboutForm" ng-submit="about.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group" ng-class="{ 'has-error': about.error.cloudronName }">
|
||||
<label class="control-label">{{ 'branding.cloudronName' | tr }}</label>
|
||||
<div class="control-label" ng-show="about.error.cloudronName">{{about.error.cloudronName}}</div>
|
||||
<input type="text" class="form-control" id="inputCloudronName" name="name" ng-model="about.cloudronName" ng-minlength="1" maxlength="64" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div>
|
||||
<label class="control-label">{{ 'branding.logo' | tr }}</label>
|
||||
</div>
|
||||
<div class="branding-avatar" ng-click="avatarChange.showChangeAvatar()">
|
||||
<img ng-src="{{ about.avatarUrl() }}"/>
|
||||
<div class="overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="(!about.avatar && !aboutForm.$dirty) || aboutForm.$invalid || about.busy"/>
|
||||
</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="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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'branding.footer.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" ng-show="config.features.branding">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form role="form" name="footerForm" autocomplete="off">
|
||||
<p>{{ 'branding.footer.description' | tr }} <sup><a ng-href="https://docs.cloudron.io/branding/#footer" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
|
||||
<textarea name="footer" class="form-control" ng-model="footer.content" ng-disabled="footer.busy"></textarea>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="footer.submit()" ng-disabled="!footerForm.$dirty || footerForm.$invalid || footer.busy"><i class="fa fa-circle-notch fa-spin" ng-show="footer.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" ng-hide="config.features.branding">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{{ 'branding.footer.subscriptionRequired' | tr }} <a href="" class="pull-right" ng-click="openSubscriptionSetup()">{{ 'branding.footer.setupSubscriptionNow' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,229 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular:false */
|
||||
/* global $:false */
|
||||
|
||||
angular.module('Application').controller('BrandingController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (Client.getUserInfo().role !== 'owner') $location.path('/'); });
|
||||
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
$scope.openSubscriptionSetup = function () {
|
||||
Client.openSubscriptionSetup($scope.$parent.subscription);
|
||||
};
|
||||
|
||||
$scope.avatarChange = {
|
||||
avatar: null, // { file, data, url }
|
||||
|
||||
availableAvatars: [{
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo.png',
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-green.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-orange.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-darkblue.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-red.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-yellow.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-black.png'
|
||||
}],
|
||||
|
||||
avatarUrl: function () {
|
||||
if ($scope.avatarChange.avatar) {
|
||||
return $scope.avatarChange.avatar.data || $scope.avatarChange.avatar.url;
|
||||
} else {
|
||||
return Client.avatar;
|
||||
}
|
||||
},
|
||||
|
||||
getBlobFromImg: function (img, callback) {
|
||||
var size = 512;
|
||||
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
|
||||
var imageDimensionRatio = img.width / img.height;
|
||||
var canvasDimensionRatio = canvas.width / canvas.height;
|
||||
var renderableHeight, renderableWidth, xStart, yStart;
|
||||
|
||||
if (imageDimensionRatio > canvasDimensionRatio) {
|
||||
renderableHeight = canvas.height;
|
||||
renderableWidth = img.width * (renderableHeight / img.height);
|
||||
xStart = (canvas.width - renderableWidth) / 2;
|
||||
yStart = 0;
|
||||
} else if (imageDimensionRatio < canvasDimensionRatio) {
|
||||
renderableWidth = canvas.width;
|
||||
renderableHeight = img.height * (renderableWidth / img.width);
|
||||
xStart = 0;
|
||||
yStart = (canvas.height - renderableHeight) / 2;
|
||||
} else {
|
||||
renderableHeight = canvas.height;
|
||||
renderableWidth = canvas.width;
|
||||
xStart = 0;
|
||||
yStart = 0;
|
||||
}
|
||||
|
||||
var ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(img, xStart, yStart, renderableWidth, renderableHeight);
|
||||
|
||||
canvas.toBlob(callback);
|
||||
},
|
||||
|
||||
setPreviewAvatar: function (avatar) {
|
||||
$scope.avatarChange.avatar = avatar;
|
||||
},
|
||||
|
||||
showChangeAvatar: function () {
|
||||
$scope.avatarChange.avatar = $scope.about.avatar;
|
||||
$('#avatarChangeModal').modal('show');
|
||||
},
|
||||
|
||||
showCustomAvatarSelector: function () {
|
||||
$('#avatarFileInput').click();
|
||||
},
|
||||
|
||||
setAvatar: function () {
|
||||
if (angular.equals($scope.about.avatar, $scope.avatarChange.avatar)) return $('#avatarChangeModal').modal('hide'); // nothing changed
|
||||
|
||||
$scope.about.avatar = $scope.avatarChange.avatar;
|
||||
|
||||
// get the blob now, we cannot get it if dialog is hidden
|
||||
var img = document.getElementById('previewAvatar');
|
||||
$scope.avatarChange.getBlobFromImg(img, function (blob) {
|
||||
$scope.about.avatarBlob = blob;
|
||||
|
||||
$('#avatarChangeModal').modal('hide');
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
$('#avatarFileInput').get(0).onchange = function (event) {
|
||||
var fr = new FileReader();
|
||||
fr.onload = function () {
|
||||
$scope.$apply(function () {
|
||||
var tmp = {
|
||||
file: event.target.files[0],
|
||||
data: fr.result,
|
||||
url: null
|
||||
};
|
||||
|
||||
$scope.avatarChange.availableAvatars.push(tmp);
|
||||
$scope.avatarChange.setPreviewAvatar(tmp);
|
||||
});
|
||||
};
|
||||
fr.readAsDataURL(event.target.files[0]);
|
||||
};
|
||||
|
||||
$scope.about = {
|
||||
busy: false,
|
||||
error: {},
|
||||
cloudronName: '',
|
||||
avatar: null,
|
||||
avatarBlob: null,
|
||||
|
||||
avatarUrl: function () {
|
||||
if ($scope.about.avatar) {
|
||||
return $scope.about.avatar.data || $scope.about.avatar.url;
|
||||
} else {
|
||||
return Client.avatar;
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function () {
|
||||
$scope.about.cloudronName = $scope.config.cloudronName;
|
||||
$scope.about.avatar = null;
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.about.error.name = null;
|
||||
$scope.about.busy = true;
|
||||
|
||||
var NOOP = function (next) { return next(); };
|
||||
var changeCloudronName = $scope.about.cloudronName !== $scope.config.cloudronName ? Client.changeCloudronName.bind(null, $scope.about.cloudronName) : NOOP;
|
||||
|
||||
changeCloudronName(function (error) {
|
||||
if (error) {
|
||||
$scope.about.busy = false;
|
||||
if (error.statusCode === 400) {
|
||||
$scope.about.error.cloudronName = error.message || 'Invalid name';
|
||||
$('#inputCloudronName').focus();
|
||||
} else {
|
||||
console.error('Unable to change name.', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var changeAvatar = $scope.about.avatar ? Client.changeCloudronAvatar.bind(null, $scope.about.avatarBlob) : NOOP;
|
||||
|
||||
changeAvatar(function (error) {
|
||||
if (error) {
|
||||
$scope.about.busy = false;
|
||||
console.error('Unable to change avatar.', error);
|
||||
return;
|
||||
}
|
||||
|
||||
Client.refreshConfig(function () {
|
||||
if ($scope.about.avatar) Client.resetAvatar();
|
||||
|
||||
$scope.aboutForm.$setPristine();
|
||||
$scope.about.avatar = null;
|
||||
$scope.about.busy = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.footer = {
|
||||
content: '',
|
||||
busy: false,
|
||||
|
||||
refresh: function () {
|
||||
Client.getFooter(function (error, result) {
|
||||
if (error) return console.error('Failed to get footer.', error);
|
||||
|
||||
$scope.footer.content = result;
|
||||
});
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.footer.busy = true;
|
||||
|
||||
Client.setFooter($scope.footer.content.trim(), function (error) {
|
||||
if (error) return console.error('Failed to set footer.', error);
|
||||
|
||||
Client.refreshConfig(function () {
|
||||
$scope.footer.busy = false;
|
||||
$scope.footerForm.$setPristine();
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
$scope.about.refresh();
|
||||
$scope.footer.refresh();
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
+275
-196
@@ -1,188 +1,244 @@
|
||||
<!-- Modal subscription -->
|
||||
<div class="modal fade" id="subscriptionRequiredModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'domains.subscriptionRequired.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html="'domains.subscriptionRequired.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-success" ng-click="openSubscriptionSetup()">{{ 'domains.subscriptionRequired.setupAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- modal domain add/configure -->
|
||||
<div class="modal fade" id="domainConfigureModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" ng-show="domainConfigure.adding">Add Domain</h4>
|
||||
<h4 class="modal-title" ng-hide="domainConfigure.adding">Configure {{ domainConfigure.domain.domain }}</h4>
|
||||
<h4 class="modal-title" ng-show="domainConfigure.adding">{{ 'domains.domainDialog.addTitle' | tr }}</h4>
|
||||
<h4 class="modal-title" ng-hide="domainConfigure.adding">{{ 'domains.domainDialog.editTitle' | tr:{ domain: domainConfigure.domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="domainConfigureForm" role="form" novalidate ng-submit="domainConfigure.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<p class="has-error text-center" ng-show="domainConfigure.error">{{ domainConfigure.error }}</p>
|
||||
<p ng-show="domainConfigure.adding" ng-bind-html="'domains.domainDialog.addDescription' | tr"></p>
|
||||
|
||||
<div class="form-group" ng-show="domainConfigure.adding">
|
||||
<label class="control-label">Domain name</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.newDomain" name="newDomain" ng-disabled="domainConfigure.busy" placeholder="example.com" ng-required="domainConfigure.adding" autofocus>
|
||||
<form name="domainConfigureForm" role="form" novalidate ng-submit="domainConfigure.submit()" autocomplete="off">
|
||||
<p class="has-error text-center" ng-show="domainConfigure.error">{{ domainConfigure.error }}</p>
|
||||
|
||||
<div class="form-group" ng-show="domainConfigure.adding">
|
||||
<label class="control-label">{{ 'domains.domainDialog.domain' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.newDomain" name="newDomain" ng-disabled="domainConfigure.busy" placeholder="example.com" ng-required="domainConfigure.adding" autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'domains.domainDialog.provider' | tr }} <sup><a ng-href="https://docs.cloudron.io/domains/#dns-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="domainConfigure.provider" ng-options="a.value as a.name for a in dnsProvider" ng-change="domainConfigure.setDefaultTlsProvider()"></select>
|
||||
</div>
|
||||
|
||||
<!-- Route53 -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'route53'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.route53AccessKeyId' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.accessKeyId" name="accessKeyId" ng-disabled="domainConfigure.busy" ng-minlength="16" ng-maxlength="32" ng-required="domainConfigure.provider === 'route53'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'route53'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.route53SecretAccessKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.secretAccessKey" name="secretAccessKey" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'route53'">
|
||||
</div>
|
||||
|
||||
<!-- Google Cloud DNS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'gcdns'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.gcdnsServiceAccountKey' | tr }}</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="domainConfigure.gcdnsKey.keyFileName" id="gcdnsKeyInput" name="cert" onclick="getElementById('gcdnsKeyFileInput').click();" style="cursor: pointer;" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'gcdns'">
|
||||
<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': false }" ng-show="domainConfigure.provider === 'digitalocean'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.digitalOceanToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.digitalOceanToken" name="digitalOceanToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'digitalocean'">
|
||||
</div>
|
||||
|
||||
<!-- Gandi -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'gandi'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.gandiApiKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.gandiApiKey" name="gandiApiKey" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'gandi'">
|
||||
</div>
|
||||
|
||||
<!-- GoDaddy -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'godaddy'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.goDaddyApiKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.godaddyApiKey" name="apiKey" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'godaddy'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'godaddy'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.goDaddyApiSecret' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.godaddyApiSecret" name="apiSecret" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'godaddy'">
|
||||
</div>
|
||||
|
||||
<!-- Netcup -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'netcup'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.netcupCustomerNumber' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.netcupCustomerNumber" name="netcupCustomerNumber" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'netcup'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'netcup'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.netcupApiKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.netcupApiKey" name="netcupApiKey" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'netcup'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'netcup'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.netcupApiPassword' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.netcupApiPassword" name="netcupApiPassword" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'netcup'">
|
||||
</div>
|
||||
|
||||
<!-- Cloudflare -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'cloudflare'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.cloudflareTokenType' | tr }}</label>
|
||||
<select class="form-control" ng-model="domainConfigure.cloudflareTokenType">
|
||||
<option value="GlobalApiKey">{{ 'domains.domainDialog.cloudflareTokenTypeGlobalApiKey' | tr }}</option>
|
||||
<option value="ApiToken">{{ 'domains.domainDialog.cloudflareTokenTypeApiToken' | tr }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'cloudflare'">
|
||||
<label class="control-label" ng-show="domainConfigure.cloudflareTokenType === 'GlobalApiKey'">{{ 'domains.domainDialog.cloudflareTokenTypeGlobalApiKey' | tr }}</label>
|
||||
<label class="control-label" ng-show="domainConfigure.cloudflareTokenType === 'ApiToken'">{{ 'domains.domainDialog.cloudflareTokenTypeApiToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.cloudflareToken" name="cloudflareToken" ng-required="domainConfigure.provider === 'cloudflare'" ng-disabled="domainConfigure.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'cloudflare' && domainConfigure.cloudflareTokenType === 'GlobalApiKey'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.cloudflareEmail' | tr }}</label>
|
||||
<input type="email" class="form-control" ng-model="domainConfigure.cloudflareEmail" name="cloudflareEmail" ng-required="domainConfigure.provider === 'cloudflare' && domainConfigure.cloudflareTokenType === 'GlobalApiKey'" ng-disabled="domainConfigure.busy">
|
||||
</div>
|
||||
|
||||
<!-- Linode -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'linode'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.linodeToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.linodeToken" name="linodeToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'linode'">
|
||||
</div>
|
||||
|
||||
<!-- Vultr -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'vultr'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.vultrToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.vultrToken" name="vultrToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'vultr'">
|
||||
</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>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.nameComUsername" name="nameComUsername" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'namecom'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecom'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.nameComApiToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.nameComToken" name="nameComToken" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'namecom'">
|
||||
</div>
|
||||
|
||||
<!-- Namecheap -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecheap'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.namecheapUsername' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.namecheapUsername" name="namecheapUsername" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'namecheap'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecheap'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.namecheapApiKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.namecheapApiKey" name="namecheapApiKey" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'namecheap'">
|
||||
</div>
|
||||
|
||||
<p class="small text-info text-bold" ng-show="domainConfigure.provider === 'namecheap'" ng-bind-html="'domains.domainDialog.namecheapInfo' | tr"></p>
|
||||
<p class="small text-info text-bold" ng-show="domainConfigure.provider === 'wildcard'" ng-bind-html="'domains.domainDialog.wildcardInfo' | tr:{ domain: domainConfigure.adding ? domainConfigure.newDomain : domainConfigure.domain.domain }"></p>
|
||||
<p class="small text-info text-bold" ng-show="domainConfigure.provider === 'manual'" ng-bind-html="'domains.domainDialog.manualInfo' | tr"></p>
|
||||
<p class="small text-info text-bold" ng-show="needsPort80(domainConfigure.provider, domainConfigure.tlsConfig.provider)" ng-bind-html="'domains.domainDialog.letsEncryptInfo' | tr"></p>
|
||||
|
||||
<a href="" ng-click="domainConfigure.advancedVisible = true" ng-hide="domainConfigure.advancedVisible">{{ 'domains.domainDialog.advancedAction' | tr }}</a>
|
||||
<div uib-collapse="!domainConfigure.advancedVisible">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'domains.domainDialog.zoneName' | tr }} <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="domainConfigure.zoneName" name="zoneName" ng-disabled="domainConfigure.busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">DNS Provider <sup><a ng-href="{{ config.webServerOrigin }}/documentation/domains/#dns-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="domainConfigure.provider" ng-options="a.value as a.name for a in dnsProvider" ng-change="domainConfigure.setDefaultTlsProvider()"></select>
|
||||
<label class="control-label">{{ 'domains.domainDialog.certProvider' | tr }} <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="domainConfigure.tlsConfig.provider" ng-options="a.value as a.name for a in tlsProvider"></select>
|
||||
</div>
|
||||
|
||||
<!-- Route53 -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'route53'">
|
||||
<label class="control-label">Access Key Id</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.accessKeyId" name="accessKeyId" ng-disabled="domainConfigure.busy" ng-minlength="16" ng-maxlength="32" ng-required="domainConfigure.provider === 'route53'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'route53'">
|
||||
<label class="control-label">Secret Access Key</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.secretAccessKey" name="secretAccessKey" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'route53'">
|
||||
<!-- Fallback certificate -->
|
||||
<div ng-show="domainConfigure.tlsConfig.provider !== 'fallback'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.fallbackCert' | tr }}</label>
|
||||
<p ng-bind-html="'domains.domainDialog.fallbackCertInfo' | tr"></p>
|
||||
</div>
|
||||
|
||||
<!-- Google Cloud DNS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'gcdns'">
|
||||
<label class="control-label">Service Account Key</label>
|
||||
<div ng-show="domainConfigure.tlsConfig.provider === 'fallback'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.fallbackCertCustomCert' | tr }}</label>
|
||||
<p ng-bind-html="'domains.domainDialog.fallbackCertCustomCertInfo' | tr:{ customCertLink: 'https://docs.cloudron.io/certificates/#custom-certificates' }"></p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (!fallbackCert.cert.$dirty && fallbackCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="gcdnsKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="domainConfigure.gcdnsKey.keyFileName" id="gcdnsKeyInput" name="cert" onclick="getElementById('gcdnsKeyFileInput').click();" style="cursor: pointer;" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'gcdns'">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('gcdnsKeyFileInput').click();"></i>
|
||||
</span>
|
||||
<input type="file" id="fallbackCertFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="{{ 'domains.domainDialog.fallbackCertCertificatePlaceholder' | tr }}" ng-model="domainConfigure.fallbackCert.certificateFileName" name="cert" onclick="getElementById('fallbackCertFileInput').click();" style="cursor: pointer;" ng-disabled="domainConfigure.busy">
|
||||
<span class="input-group-addon"><i class="fa fa-upload" onclick="getElementById('fallbackCertFileInput').click();"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!fallbackCert.key.$dirty && fallbackCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="fallbackKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="{{ 'domains.domainDialog.fallbackCertKeyPlaceholder' | tr }}" ng-model="domainConfigure.fallbackCert.keyFileName" id="fallbackKeyInput" name="key" onclick="getElementById('fallbackKeyFileInput').click();" style="cursor: pointer;" ng-disabled="domainConfigure.busy">
|
||||
<span class="input-group-addon"><i class="fa fa-upload" onclick="getElementById('fallbackKeyFileInput').click();"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DigitalOcean -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'digitalocean'">
|
||||
<label class="control-label">DigitalOcean Token</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.digitalOceanToken" name="digitalOceanToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'digitalocean'">
|
||||
</div>
|
||||
</div> <!-- advanced -->
|
||||
|
||||
<!-- Gandi -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'gandi'">
|
||||
<label class="control-label">Gandi API Key</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.gandiApiKey" name="gandiApiKey" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'gandi'">
|
||||
</div>
|
||||
|
||||
<!-- GoDaddy -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'godaddy'">
|
||||
<label class="control-label">API Key</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.godaddyApiKey" name="apiKey" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'godaddy'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'godaddy'">
|
||||
<label class="control-label">API Secret</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.godaddyApiSecret" name="apiSecret" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'godaddy'">
|
||||
</div>
|
||||
|
||||
<!-- Cloudflare -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'cloudflare'">
|
||||
<label class="control-label">Token Type</label>
|
||||
<select class="form-control" ng-model="domainConfigure.cloudflareTokenType">
|
||||
<option value="GlobalApiKey">Global API Key</option>
|
||||
<option value="ApiToken">API Token</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'cloudflare'">
|
||||
<label class="control-label" ng-show="domainConfigure.cloudflareTokenType === 'GlobalApiKey'">Global API Key</label>
|
||||
<label class="control-label" ng-show="domainConfigure.cloudflareTokenType === 'ApiToken'">Api Token</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.cloudflareToken" name="cloudflareToken" placeholder="API Key/Token" ng-required="domainConfigure.provider === 'cloudflare'" ng-disabled="domainConfigure.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'cloudflare' && domainConfigure.cloudflareTokenType === 'GlobalApiKey'">
|
||||
<label class="control-label">Cloudflare Email</label>
|
||||
<input type="email" class="form-control" ng-model="domainConfigure.cloudflareEmail" name="cloudflareEmail" placeholder="Cloudflare Account Email" ng-required="domainConfigure.provider === 'cloudflare' && domainConfigure.cloudflareTokenType === 'GlobalApiKey'" ng-disabled="domainConfigure.busy">
|
||||
</div>
|
||||
|
||||
<!-- Name.com -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecom'">
|
||||
<label class="control-label">Name.com Username</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.nameComUsername" name="nameComUsername" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'namecom'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecom'">
|
||||
<label class="control-label">API Token</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.nameComToken" name="nameComToken" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'namecom'">
|
||||
</div>
|
||||
|
||||
<!-- Namecheap -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecheap'">
|
||||
<label class="control-label">Namecheap Username</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.namecheapUsername" name="namecheapUsername" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'namecheap'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecheap'">
|
||||
<label class="control-label">API Key</label>
|
||||
<p class="small text-info" ng-show="domainConfigure.provider === 'namecheap'">
|
||||
<b>The server IP needs to be whitelisted for this API Key.</b>
|
||||
</p>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.namecheapApiKey" name="namecheapApiKey" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'namecheap'">
|
||||
</div>
|
||||
|
||||
<p class="small text-info" ng-show="domainConfigure.provider === 'wildcard'">
|
||||
Setup <i>A</i> records for <b>*.{{ domainConfigure.newDomain || domainConfigure.domain.domain }}</b> and <b>{{ domainConfigure.newDomain || domainConfigure.domain.domain }}</b> to this server's IP.
|
||||
</p>
|
||||
|
||||
<p class="small text-info" ng-show="domainConfigure.provider === 'manual'">
|
||||
<b>All DNS records have to be setup manually before each app installation.</b>
|
||||
</p>
|
||||
|
||||
<p class="small text-info" ng-show="needsPort80(domainConfigure.provider, domainConfigure.tlsConfig.provider)">Let's Encrypt requires your server to be reachable on port 80</p>
|
||||
|
||||
<a href="" ng-click="domainConfigure.advancedVisible = true" ng-hide="domainConfigure.advancedVisible">Advanced settings...</a>
|
||||
<div uib-collapse="!domainConfigure.advancedVisible">
|
||||
|
||||
<div ng-show="false">
|
||||
<label class="control-label">
|
||||
<input type="checkbox" ng-model="domainConfigure.hyphenatedSubdomains" name="hyphenatedSubdomains" ng-disabled="domainConfigure.busy"/> Hyphenate Subdomains
|
||||
</label>
|
||||
<p>When enabled, apps are installed into <code><location>-<domain></code></p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">Zone Name (Optional) <sup><a ng-href="{{ config.webServerOrigin }}/documentation/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="domainConfigure.zoneName" name="zoneName" ng-disabled="domainConfigure.busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">Certificate Provider <sup><a ng-href="{{ config.webServerOrigin }}/documentation/certificates/#certificate-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="domainConfigure.tlsConfig.provider" ng-options="a.value as a.name for a in tlsProvider"></select>
|
||||
</div>
|
||||
|
||||
<!-- Fallback certificate -->
|
||||
<div ng-show="domainConfigure.tlsConfig.provider !== 'fallback'">
|
||||
<label class="control-label">Fallback Certificate (optional)</label>
|
||||
<p>
|
||||
Certificates are automatically obtained and renewed from <a href="https://letsencrypt.org/" target="_blank">Let’s Encrypt</a>. See the current rate limit <a href="https://letsencrypt.org/docs/rate-limits/" target="_blank">here</a>.
|
||||
This wildcard certificate will be used should getting a Let’s Encrypt certificate fail. If not provided, an automatically generated self-signed certificate will be used as fallback.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div ng-show="domainConfigure.tlsConfig.provider === 'fallback'">
|
||||
<label class="control-label">Custom Certificate</label>
|
||||
<p>
|
||||
This <a ng-href="{{ config.webServerOrigin }}/documentation/certificates/#custom-certificates" target="_blank">wildcard certificate</a> will be used for all apps on this domain. If not provided, a self-signed certificate will be automatically generated.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (!fallbackCert.cert.$dirty && fallbackCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="fallbackCertFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Certificate" ng-model="domainConfigure.fallbackCert.certificateFileName" name="cert" onclick="getElementById('fallbackCertFileInput').click();" style="cursor: pointer;" ng-disabled="domainConfigure.busy">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('fallbackCertFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!fallbackCert.key.$dirty && fallbackCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="fallbackKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Key" ng-model="domainConfigure.fallbackCert.keyFileName" id="fallbackKeyInput" name="key" onclick="getElementById('fallbackKeyFileInput').click();" style="cursor: pointer;" ng-disabled="domainConfigure.busy">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('fallbackKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- advanced -->
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="domainConfigureForm.$invalid || domainConfigure.busy"/>
|
||||
</fieldset>
|
||||
<input class="ng-hide" type="submit" ng-disabled="domainConfigureForm.$invalid || domainConfigure.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="domainConfigure.submit()" ng-disabled="domainConfigureForm.$invalid || domainConfigure.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="domainConfigure.busy"></i> Save
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="domainConfigure.submit()" ng-disabled="domainConfigureForm.$invalid || domainConfigure.busy"><i class="fa fa-circle-notch fa-spin" ng-show="domainConfigure.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- modal domain wellknown -->
|
||||
<div class="modal fade" id="domainWellKnownModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'domains.domainWellKnown.title' | tr:{ domain: domainWellKnown.domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html="'domains.domainDialog.wellKnownDescription' | tr:{ domain: domainWellKnown.domain.domain, docsLink: 'https://docs.cloudron.io/domains/#well-known-locations' }"></p>
|
||||
|
||||
<form name="domainWellKnownForm" role="form" novalidate ng-submit="domainWellKnown.submit()" autocomplete="off">
|
||||
<p class="has-error text-center" ng-show="domainWellKnown.error">{{ domainWellKnown.error }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'domains.domainDialog.matrixHostname' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainWellKnown.matrixHostname" name="matrixHostname" ng-disabled="domainWellKnown.busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'domains.domainDialog.mastodonHostname' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainWellKnown.mastodonHostname" name="mastodonHostname" ng-disabled="domainWellKnown.busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'domains.domainDialog.jitsiHostname' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainWellKnown.jitsiHostname" name="jitsiHostname" ng-disabled="domainWellKnown.busy">
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="domainWellKnownForm.$invalid || domainWellKnown.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="domainWellKnown.submit()" ng-disabled="domainWellKnownForm.$invalid || domainWellKnown.busy"><i class="fa fa-circle-notch fa-spin" ng-show="domainWellKnown.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,18 +249,16 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Really remove {{ domainRemove.domain.domain }} ?</h4>
|
||||
<h4 class="modal-title">{{ 'domains.removeDialog.title' | tr:{ domain: domainRemove.domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
This will delete the domain <code>{{ domainRemove.domain.domain }}</code>.
|
||||
<div>
|
||||
<br/>
|
||||
<span class="has-error" ng-show="domainRemove.error">{{ domainRemove.error }}</span>
|
||||
</div>
|
||||
<p ng-bind-html="'domains.removeDialog.description' | tr:{ domain: domainRemove.domain.domain }"></p>
|
||||
<br/>
|
||||
<span class="has-error" ng-show="domainRemove.error">{{ domainRemove.error }}</span>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="domainRemove.submit()" ng-disabled="domainRemove.busy"><i class="fa fa-circle-notch fa-spin" ng-show="domainRemove.busy"></i> Remove</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="domainRemove.submit()" ng-disabled="domainRemove.busy"><i class="fa fa-circle-notch fa-spin" ng-show="domainRemove.busy"></i> {{ 'domains.removeDialog.removeAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -212,7 +266,7 @@
|
||||
|
||||
<div class="content">
|
||||
<div class="text-left">
|
||||
<h1>Domains <button class="btn btn-primary btn-outline pull-right" ng-click="domainConfigure.show()"><i class="fa fa-plus"></i> Add Domain</button></h1>
|
||||
<h1>{{ 'domains.title' | tr }} <button class="btn btn-primary btn-outline pull-right" ng-click="domainAdd.show()"><i class="fa fa-plus"></i> {{ 'domains.addDomain' | tr }}</button></h1>
|
||||
</div>
|
||||
|
||||
<div class="card card-large">
|
||||
@@ -226,22 +280,23 @@
|
||||
<table class="table table-hover" style="margin-top: 10px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th class="text-left hidden-xs hidden-sm">Provider</th>
|
||||
<th style="width: 100px" class="text-right">Actions</th>
|
||||
<th>{{ 'domains.domain' | tr }}</th>
|
||||
<th class="text-left hidden-xs hidden-sm">{{ 'domains.provider' | tr }}</th>
|
||||
<th style="width: 100px" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="domain in domains">
|
||||
<td class="elide-table-cell hand" ng-click="domain.provider !== 'caas' && domainConfigure.show(domain)">
|
||||
<td class="elide-table-cell hand" ng-click="domainConfigure.show(domain)">
|
||||
{{ domain.domain }}
|
||||
</td>
|
||||
<td class="text-left elide-table-cell hidden-xs hidden-sm hand" ng-click="domain.provider !== 'caas' && domainConfigure.show(domain)">
|
||||
<td class="text-left elide-table-cell hidden-xs hidden-sm hand" ng-click="domainConfigure.show(domain)">
|
||||
{{ prettyProviderName(domain) }}
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-default" ng-click="domainConfigure.show(domain)" ng-show="domain.provider !== 'caas'" title="Edit Domain"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button class="btn btn-xs btn-danger" ng-click="domainRemove.show(domain)" ng-show="domain.provider !== 'caas'" title="Remove Domain"><i class="far fa-trash-alt"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="domainWellKnown.show(domain)" title="{{ 'domains.tooltipWellKnown' | tr }}"><i class="fa fa-atlas"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="domainConfigure.show(domain)" title="{{ 'domains.tooltipEdit' | tr }}"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button class="btn btn-xs btn-danger" ng-click="domainRemove.show(domain)" title="{{ 'domains.tooltipRemove' | tr }}" ng-disabled="domain.domain === config.adminDomain"><i class="far fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -251,15 +306,13 @@
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>Renew certificates</h3>
|
||||
<h3>{{ 'domains.renewCerts.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>
|
||||
Cloudron renews Let's Encrypt certificates automatically. Use this option to trigger a renewal immediately.
|
||||
</p>
|
||||
<p ng-bind-html="'domains.renewCerts.description' | tr"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -272,30 +325,56 @@
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-12">
|
||||
<p ng-show="renewCerts.busy">{{ renewCerts.message }}</p>
|
||||
<p ng-hide="renewCerts.busy">
|
||||
<div class="has-error" ng-show="!renewCerts.active">{{ renewCerts.errorMessage }}</div>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-right">
|
||||
<button class="btn btn-outline btn-primary" ng-click="renewCerts.renew()" ng-disabled="renewCerts.busy" style="margin-right: 10px">Renew All Certs</button>
|
||||
<a class="btn btn-primary pull-right" ng-href="/logs.html?taskId={{renewCerts.taskId}}" ng-disabled="!renewCerts.taskId" target="_blank">Show Logs</a>
|
||||
<a class="btn btn-primary pull-right" ng-href="/logs.html?taskId={{renewCerts.taskId}}" ng-disabled="!renewCerts.taskId" target="_blank">{{ 'domains.renewCerts.showLogsAction' | tr }}</a>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="renewCerts.renew()" ng-disabled="renewCerts.busy" style="margin-right: 10px">{{ 'domains.renewCerts.renewAllAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>Change Dashboard Domain</h3>
|
||||
<h3>{{ 'domains.syncDns.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<p>
|
||||
This will move the dashboard to the <code>my</code>subdomain of the selected domain. Email server will be reconfigured to
|
||||
send notifications from this domain.
|
||||
</p>
|
||||
<div class="col-md-12">
|
||||
<p ng-bind-html="'domains.syncDns.description' | tr"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<div ng-show="syncDns.busy" class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ syncDns.percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-show="syncDns.busy">{{ syncDns.message }}</p>
|
||||
<p ng-hide="syncDns.busy">
|
||||
<div class="has-error" ng-show="!syncDns.active">{{ syncDns.errorMessage }}</div>
|
||||
</p>
|
||||
<a class="btn btn-primary pull-right" ng-href="/logs.html?taskId={{syncDns.taskId}}" ng-disabled="!syncDns.taskId" target="_blank">{{ 'domains.syncDns.showLogsAction' | tr }}</a>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="syncDns.sync()" ng-disabled="syncDns.busy" style="margin-right: 10px">{{ 'domains.syncDns.syncAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'domains.changeDashboardDomain.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<p ng-bind-html="'domains.changeDashboardDomain.description' | tr"></p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<select class="form-control pull-right" style="display: inline-block; width: 200px;" ng-model="changeDashboard.selectedDomain" ng-options="a.domain for a in domains"></select>
|
||||
@@ -318,9 +397,9 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-right">
|
||||
<button class="btn btn-outline btn-primary" ng-click="changeDashboard.change()" ng-hide="changeDashboard.busy" ng-disabled="changeDashboard.selectedDomain.domain === changeDashboard.adminDomain.domain">Change Domain</button>
|
||||
<button class="btn btn-outline btn-danger" ng-click="changeDashboard.stop()" ng-show="changeDashboard.busy" style="margin-right: 10px">Cancel</button>
|
||||
<a class="btn btn-primary pull-right" ng-href="/logs.html?taskId={{changeDashboard.taskId}}" ng-show="changeDashboard.busy" target="_blank">Show Logs</a>
|
||||
<button class="btn btn-outline btn-primary" ng-click="changeDashboard.change()" ng-hide="changeDashboard.busy" ng-disabled="changeDashboard.selectedDomain.domain === changeDashboard.adminDomain.domain">{{ 'domains.changeDashboardDomain.changeAction' | tr }}</button>
|
||||
<button class="btn btn-outline btn-danger" ng-click="changeDashboard.stop()" ng-show="changeDashboard.busy" style="margin-right: 10px">{{ 'domains.changeDashboardDomain.cancelAction' | tr }}</button>
|
||||
<a class="btn btn-primary pull-right" ng-href="/logs.html?taskId={{changeDashboard.taskId}}" ng-show="changeDashboard.busy" target="_blank">{{ 'domains.changeDashboardDomain.showLogsAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+221
-31
@@ -1,8 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
/* global asyncForEach:false */
|
||||
/* global angular:false */
|
||||
/* global $:false */
|
||||
/* global async */
|
||||
/* global angular */
|
||||
/* global $, TASK_TYPES */
|
||||
|
||||
angular.module('Application').controller('DomainsController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
@@ -11,6 +11,15 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.domains = [];
|
||||
$scope.ready = false;
|
||||
|
||||
$scope.translationLinks = {
|
||||
linodeDocsLink: 'https://docs.cloudron.io/domains/#linode-dns',
|
||||
customCertLink: 'https://docs.cloudron.io/certificates/#custom-certificates'
|
||||
};
|
||||
|
||||
$scope.openSubscriptionSetup = function () {
|
||||
Client.openSubscriptionSetup($scope.$parent.subscription);
|
||||
};
|
||||
|
||||
// currently, validation of wildcard with various provider is done server side
|
||||
$scope.tlsProvider = [
|
||||
{ name: 'Let\'s Encrypt Prod', value: 'letsencrypt-prod' },
|
||||
@@ -28,8 +37,11 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
{ name: 'Gandi LiveDNS', value: 'gandi' },
|
||||
{ name: 'GoDaddy', value: 'godaddy' },
|
||||
{ name: 'Google Cloud DNS', value: 'gcdns' },
|
||||
{ name: 'Linode', value: 'linode' },
|
||||
{ name: 'Name.com', value: 'namecom' },
|
||||
{ name: 'Namecheap', value: 'namecheap' },
|
||||
{ name: 'Netcup', value: 'netcup' },
|
||||
{ name: 'Vultr', value: 'vultr' },
|
||||
{ name: 'Wildcard', value: 'wildcard' },
|
||||
{ name: 'Manual (not recommended)', value: 'manual' },
|
||||
{ name: 'No-op (only for development)', value: 'noop' }
|
||||
@@ -37,19 +49,21 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
|
||||
$scope.prettyProviderName = function (domain) {
|
||||
switch (domain.provider) {
|
||||
case 'caas': return 'Managed Cloudron';
|
||||
case 'route53': return 'AWS Route53';
|
||||
case 'cloudflare': return 'Cloudflare';
|
||||
case 'digitalocean': return 'DigitalOcean';
|
||||
case 'gandi': return 'Gandi LiveDNS';
|
||||
case 'namecom': return 'Name.com';
|
||||
case 'namecheap': return 'Namecheap';
|
||||
case 'gcdns': return 'Google Cloud';
|
||||
case 'godaddy': return 'GoDaddy';
|
||||
case 'manual': return 'Manual';
|
||||
case 'wildcard': return 'Wildcard';
|
||||
case 'noop': return 'No-op';
|
||||
default: return 'Unknown';
|
||||
case 'route53': return 'AWS Route53';
|
||||
case 'cloudflare': return 'Cloudflare';
|
||||
case 'digitalocean': return 'DigitalOcean';
|
||||
case 'gandi': return 'Gandi LiveDNS';
|
||||
case 'linode': return 'Linode';
|
||||
case 'namecom': return 'Name.com';
|
||||
case 'namecheap': return 'Namecheap';
|
||||
case 'netcup': return 'Netcup';
|
||||
case 'gcdns': return 'Google Cloud';
|
||||
case 'godaddy': return 'GoDaddy';
|
||||
case 'vultr': return 'Vultr';
|
||||
case 'manual': return 'Manual';
|
||||
case 'wildcard': return 'Wildcard';
|
||||
case 'noop': return 'No-op';
|
||||
default: return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -80,7 +94,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
Client.getDomains(function (error, results) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
asyncForEach(results, function (result, iteratorDone) {
|
||||
async.eachSeries(results, function (result, iteratorDone) {
|
||||
Client.getDomain(result.domain, function (error, domain) {
|
||||
if (error) return iteratorDone(error);
|
||||
|
||||
@@ -99,6 +113,106 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
});
|
||||
}
|
||||
|
||||
$scope.domainAdd = {
|
||||
show: function () {
|
||||
if ($scope.config.features.domainMaxCount && $scope.config.features.domainMaxCount <= $scope.domains.length) {
|
||||
$('#subscriptionRequiredModal').modal('show');
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.domainConfigure.show();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.domainWellKnown = {
|
||||
busy: false,
|
||||
error: null,
|
||||
|
||||
domain: null,
|
||||
mastodonHostname: '',
|
||||
matrixHostname: '',
|
||||
jitsiHostname: '',
|
||||
|
||||
reset: function () {
|
||||
$scope.domainWellKnown.busy = false;
|
||||
$scope.domainWellKnown.error = null;
|
||||
$scope.domainWellKnown.domain = null;
|
||||
|
||||
$scope.domainWellKnown.matrixHostname = '';
|
||||
$scope.domainWellKnown.mastodonHostname = '';
|
||||
$scope.domainWellKnown.jitsiHostname = '';
|
||||
},
|
||||
|
||||
show: function (domain) {
|
||||
$scope.domainWellKnown.reset();
|
||||
|
||||
$scope.domainWellKnown.domain = domain;
|
||||
|
||||
try {
|
||||
if (domain.wellKnown && domain.wellKnown['matrix/server']) {
|
||||
$scope.domainWellKnown.matrixHostname = JSON.parse(domain.wellKnown['matrix/server'])['m.server'];
|
||||
}
|
||||
if (domain.wellKnown && domain.wellKnown['host-meta']) {
|
||||
$scope.domainWellKnown.mastodonHostname = domain.wellKnown['host-meta'].match(new RegExp('template="https://(.*?)/'))[1];
|
||||
}
|
||||
if (domain.wellKnown && domain.wellKnown['matrix/client']) {
|
||||
let parsed = JSON.parse(domain.wellKnown['matrix/client']);
|
||||
if (parsed['im.vector.riot.jitsi'] && parsed['im.vector.riot.jitsi']['preferredDomain']) {
|
||||
$scope.domainWellKnown.jitsiHostname = parsed['im.vector.riot.jitsi']['preferredDomain'];
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
$('#domainWellKnownModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.domainWellKnown.busy = true;
|
||||
$scope.domainWellKnown.error = null;
|
||||
|
||||
var wellKnown = {};
|
||||
if ($scope.domainWellKnown.matrixHostname) {
|
||||
wellKnown['matrix/server'] = JSON.stringify({ 'm.server': $scope.domainWellKnown.matrixHostname });
|
||||
// https://matrix.org/docs/spec/client_server/latest#get-well-known-matrix-client
|
||||
wellKnown['matrix/client'] = JSON.stringify({
|
||||
'm.homeserver': {
|
||||
'base_url': 'https://' + $scope.domainWellKnown.matrixHostname
|
||||
},
|
||||
'im.vector.riot.jitsi': {
|
||||
'preferredDomain': $scope.domainWellKnown.jitsiHostname
|
||||
}
|
||||
});
|
||||
} else if ($scope.domainWellKnown.jitsiHostname) { // only if matrixHostname is not set
|
||||
wellKnown['matrix/client'] = JSON.stringify({
|
||||
'im.vector.riot.jitsi': {
|
||||
'preferredDomain': $scope.domainWellKnown.jitsiHostname
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ($scope.domainWellKnown.mastodonHostname) {
|
||||
wellKnown['host-meta'] = '<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
+ '<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">\n'
|
||||
+ '<Link rel="lrdd" type="application/xrd+xml" template="https://' + $scope.domainWellKnown.mastodonHostname + '/.well-known/webfinger?resource={uri}"/>\n'
|
||||
+ '</XRD>';
|
||||
}
|
||||
|
||||
Client.updateDomainWellKnown($scope.domainWellKnown.domain.domain, wellKnown, function (error) {
|
||||
$scope.domainWellKnown.busy = false;
|
||||
if (error) {
|
||||
$scope.domainWellKnown.error = error.message;
|
||||
return;
|
||||
}
|
||||
|
||||
$('#domainWellKnownModal').modal('hide');
|
||||
$scope.domainWellKnown.reset();
|
||||
refreshDomains();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// We reused configure also for adding domains to avoid much code duplication
|
||||
$scope.domainConfigure = {
|
||||
adding: false,
|
||||
@@ -119,13 +233,17 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
cloudflareToken: '',
|
||||
cloudflareEmail: '',
|
||||
cloudflareTokenType: 'GlobalApiKey',
|
||||
linodeToken: '',
|
||||
vultrToken: '',
|
||||
nameComToken: '',
|
||||
nameComUsername: '',
|
||||
namecheapUsername: '',
|
||||
namecheapApiKey: '',
|
||||
netcupCustomerNumber: '',
|
||||
netcupApiKey: '',
|
||||
netcupApiPassword: '',
|
||||
provider: 'route53',
|
||||
zoneName: '',
|
||||
hyphenatedSubdomains: false,
|
||||
|
||||
tlsConfig: {
|
||||
provider: 'letsencrypt-prod-wildcard'
|
||||
@@ -160,12 +278,16 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.domainConfigure.gcdnsKey.content = '';
|
||||
if (domain.provider === 'gcdns') {
|
||||
$scope.domainConfigure.gcdnsKey.keyFileName = domain.config.credentials && domain.config.credentials.client_email;
|
||||
|
||||
$scope.domainConfigure.gcdnsKey.content = JSON.stringify({
|
||||
project_id: domain.config.projectId,
|
||||
credentials: domain.config.credentials
|
||||
client_email: domain.config.credentials.client_email,
|
||||
private_key: domain.config.credentials.private_key
|
||||
});
|
||||
}
|
||||
$scope.domainConfigure.digitalOceanToken = domain.provider === 'digitalocean' ? domain.config.token : '';
|
||||
$scope.domainConfigure.linodeToken = domain.provider === 'linode' ? domain.config.token : '';
|
||||
$scope.domainConfigure.vultrToken = domain.provider === 'vultr' ? 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 : '';
|
||||
@@ -180,6 +302,10 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.domainConfigure.namecheapApiKey = domain.provider === 'namecheap' ? domain.config.token : '';
|
||||
$scope.domainConfigure.namecheapUsername = domain.provider === 'namecheap' ? domain.config.username : '';
|
||||
|
||||
$scope.domainConfigure.netcupCustomerNumber = domain.provider === 'netcup' ? domain.config.customerNumber : '';
|
||||
$scope.domainConfigure.netcupApiKey = domain.provider === 'netcup' ? domain.config.apiKey : '';
|
||||
$scope.domainConfigure.netcupApiPassword = domain.provider === 'netcup' ? domain.config.apiPassword : '';
|
||||
|
||||
$scope.domainConfigure.provider = domain.provider;
|
||||
|
||||
$scope.domainConfigure.tlsConfig.provider = domain.tlsConfig.provider;
|
||||
@@ -187,8 +313,6 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
if (domain.tlsConfig.wildcard) $scope.domainConfigure.tlsConfig.provider += '-wildcard';
|
||||
}
|
||||
$scope.domainConfigure.zoneName = domain.zoneName;
|
||||
|
||||
$scope.domainConfigure.hyphenatedSubdomains = !!domain.config.hyphenatedSubdomains;
|
||||
} else {
|
||||
$scope.domainConfigure.adding = true;
|
||||
}
|
||||
@@ -212,8 +336,8 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
var serviceAccountKey = JSON.parse($scope.domainConfigure.gcdnsKey.content);
|
||||
data.projectId = serviceAccountKey.project_id;
|
||||
data.credentials = {
|
||||
client_email: serviceAccountKey.credentials.client_email,
|
||||
private_key: serviceAccountKey.credentials.private_key
|
||||
client_email: serviceAccountKey.client_email,
|
||||
private_key: serviceAccountKey.private_key
|
||||
};
|
||||
|
||||
if (!data.projectId || !data.credentials || !data.credentials.client_email || !data.credentials.private_key) {
|
||||
@@ -226,6 +350,10 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
}
|
||||
} else if (provider === 'digitalocean') {
|
||||
data.token = $scope.domainConfigure.digitalOceanToken;
|
||||
} else if (provider === 'linode') {
|
||||
data.token = $scope.domainConfigure.linodeToken;
|
||||
} else if (provider === 'vultr') {
|
||||
data.token = $scope.domainConfigure.vultrToken;
|
||||
} else if (provider === 'gandi') {
|
||||
data.token = $scope.domainConfigure.gandiApiKey;
|
||||
} else if (provider === 'godaddy') {
|
||||
@@ -241,10 +369,12 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
} else if (provider === 'namecheap') {
|
||||
data.token = $scope.domainConfigure.namecheapApiKey;
|
||||
data.username = $scope.domainConfigure.namecheapUsername;
|
||||
} else if (provider === 'netcup') {
|
||||
data.customerNumber = $scope.domainConfigure.netcupCustomerNumber;
|
||||
data.apiKey = $scope.domainConfigure.netcupApiKey;
|
||||
data.apiPassword = $scope.domainConfigure.netcupApiPassword;
|
||||
}
|
||||
|
||||
data.hyphenatedSubdomains = $scope.domainConfigure.hyphenatedSubdomains;
|
||||
|
||||
var fallbackCertificate = null;
|
||||
if ($scope.domainConfigure.fallbackCert.certificateFile && $scope.domainConfigure.fallbackCert.keyFile) {
|
||||
fallbackCertificate = {
|
||||
@@ -265,7 +395,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
// choose the right api, since we reuse this for adding and configuring domains
|
||||
var func;
|
||||
if ($scope.domainConfigure.adding) func = Client.addDomain.bind(Client, $scope.domainConfigure.newDomain, $scope.domainConfigure.zoneName, provider, data, fallbackCertificate, tlsConfig);
|
||||
else func = Client.updateDomain.bind(Client, $scope.domainConfigure.domain.domain, $scope.domainConfigure.zoneName, provider, data, fallbackCertificate, tlsConfig);
|
||||
else func = Client.updateDomainConfig.bind(Client, $scope.domainConfigure.domain.domain, $scope.domainConfigure.zoneName, provider, data, fallbackCertificate, tlsConfig);
|
||||
|
||||
func(function (error) {
|
||||
$scope.domainConfigure.busy = false;
|
||||
@@ -305,12 +435,14 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.domainConfigure.nameComUsername = '';
|
||||
$scope.domainConfigure.namecheapApiKey = '';
|
||||
$scope.domainConfigure.namecheapUsername = '';
|
||||
$scope.domainConfigure.netcupCustomerNumber = '';
|
||||
$scope.domainConfigure.netcupApiKey = '';
|
||||
$scope.domainConfigure.netcupApiPassword = '';
|
||||
$scope.domainConfigure.vultrToken = '';
|
||||
|
||||
$scope.domainConfigure.tlsConfig.provider = 'letsencrypt-prod';
|
||||
$scope.domainConfigure.zoneName = '';
|
||||
|
||||
$scope.domainConfigure.hyphenatedSubdomains = false;
|
||||
|
||||
$scope.domainConfigureForm.$setPristine();
|
||||
$scope.domainConfigureForm.$setUntouched();
|
||||
}
|
||||
@@ -324,7 +456,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
taskId: '',
|
||||
|
||||
checkStatus: function () {
|
||||
Client.getLatestTaskByType('renewcerts', function (error, task) {
|
||||
Client.getLatestTaskByType(TASK_TYPES.TASK_RENEW_CERTS, function (error, task) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!task) return;
|
||||
@@ -360,7 +492,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
$scope.renewCerts.message = '';
|
||||
$scope.renewCerts.errorMessage = '';
|
||||
|
||||
Client.renewCerts(null /* all domains */, function (error, taskId) {
|
||||
Client.renewCerts(function (error, taskId) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
$scope.renewCerts.errorMessage = error.message;
|
||||
@@ -374,6 +506,64 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
}
|
||||
};
|
||||
|
||||
$scope.syncDns = {
|
||||
busy: false,
|
||||
percent: 0,
|
||||
message: '',
|
||||
errorMessage: '',
|
||||
taskId: '',
|
||||
|
||||
checkStatus: function () {
|
||||
Client.getLatestTaskByType(TASK_TYPES.TASK_SYNC_DNS_RECORDS, function (error, task) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!task) return;
|
||||
|
||||
$scope.syncDns.taskId = task.id;
|
||||
$scope.syncDns.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
Client.getTask($scope.syncDns.taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.syncDns.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
$scope.syncDns.busy = false;
|
||||
$scope.syncDns.message = '';
|
||||
$scope.syncDns.percent = 100; // indicates that 'result' is valid
|
||||
$scope.syncDns.errorMessage = data.success ? '' : data.error.message;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.syncDns.busy = true;
|
||||
$scope.syncDns.percent = data.percent;
|
||||
$scope.syncDns.message = data.message;
|
||||
window.setTimeout($scope.syncDns.updateStatus, 500);
|
||||
});
|
||||
},
|
||||
|
||||
sync: function () {
|
||||
$scope.syncDns.busy = true;
|
||||
$scope.syncDns.percent = 0;
|
||||
$scope.syncDns.message = '';
|
||||
$scope.syncDns.errorMessage = '';
|
||||
|
||||
Client.setDnsRecords({}, function (error, taskId) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
$scope.syncDns.errorMessage = error.message;
|
||||
|
||||
$scope.syncDns.busy = false;
|
||||
} else {
|
||||
$scope.syncDns.taskId = taskId;
|
||||
$scope.syncDns.updateStatus();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.domainRemove = {
|
||||
busy: false,
|
||||
error: null,
|
||||
@@ -517,7 +707,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
|
||||
// setup all the dialog focus handling
|
||||
['domainConfigureModal', 'domainRemoveModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
$(this).find('[autofocus]:first').focus();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+565
-404
File diff suppressed because it is too large
Load Diff
+402
-81
@@ -1,23 +1,47 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular:false */
|
||||
/* global $:false */
|
||||
/* global angular */
|
||||
/* global $ */
|
||||
/* global async */
|
||||
|
||||
angular.module('Application').controller('EmailController', ['$scope', '$location', '$timeout', '$routeParams', 'Client', function ($scope, $location, $timeout, $routeParams, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
angular.module('Application').controller('EmailController', ['$scope', '$location', '$translate', '$timeout', '$route', '$routeParams', 'Client', function ($scope, $location, $translate, $timeout, $route, $routeParams, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastMailManager) $location.path('/'); });
|
||||
|
||||
$scope.user = Client.getUserInfo();
|
||||
|
||||
// Avoid full reload on path change
|
||||
// https://stackoverflow.com/a/22614334
|
||||
// reloadOnUrl: false in $routeProvider did not work!
|
||||
var lastRoute = $route.current;
|
||||
$scope.$on('$locationChangeSuccess', function (/* event */) {
|
||||
if (lastRoute.$$route.originalPath === $route.current.$$route.originalPath) {
|
||||
$route.current = lastRoute;
|
||||
}
|
||||
});
|
||||
|
||||
var domainName = $routeParams.domain;
|
||||
if (!domainName) return $location.path('/email');
|
||||
|
||||
$scope.setView = function (view, setAlways) {
|
||||
if (!setAlways && !$scope.ready) return;
|
||||
if ($scope.view === view) return;
|
||||
|
||||
$route.updateParams({ view: view });
|
||||
$scope.view = view;
|
||||
$scope.activeTab = view;
|
||||
};
|
||||
|
||||
$scope.ready = false;
|
||||
$scope.refreshBusy = true;
|
||||
$scope.client = Client;
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.apps = Client.getInstalledApps();
|
||||
$scope.users = [];
|
||||
$scope.owners = []; // users + groups
|
||||
$scope.incomingDomains = [];
|
||||
$scope.domain = null;
|
||||
$scope.adminDomain = null;
|
||||
$scope.diskUsage = {};
|
||||
|
||||
$scope.expectedDnsRecords = {
|
||||
mx: { },
|
||||
@@ -26,6 +50,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
dmarc: { },
|
||||
ptr: { }
|
||||
};
|
||||
|
||||
$scope.expectedDnsRecordsTypes = [
|
||||
{ name: 'MX', value: 'mx' },
|
||||
{ name: 'DKIM', value: 'dkim' },
|
||||
@@ -34,8 +59,13 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
{ name: 'PTR', value: 'ptr' }
|
||||
];
|
||||
|
||||
$scope.openSubscriptionSetup = function () {
|
||||
Client.openSubscriptionSetup($scope.$parent.subscription);
|
||||
};
|
||||
|
||||
$scope.catchall = {
|
||||
mailboxes: [],
|
||||
availableMailboxes: [],
|
||||
busy: false,
|
||||
|
||||
submit: function () {
|
||||
@@ -51,9 +81,15 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
},
|
||||
|
||||
refresh: function () {
|
||||
$scope.catchall.mailboxes = $scope.domain.mailConfig.catchAll.map(function (name) {
|
||||
return $scope.mailboxes.mailboxes.find(function (m) { return m.name === name; });
|
||||
}).filter(function (m) { return !!m; });
|
||||
Client.listMailboxes($scope.domain.domain, '', 1, 1000, function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.catchall.availableMailboxes = result;
|
||||
|
||||
$scope.catchall.mailboxes = $scope.domain.mailConfig.catchAll.map(function (name) {
|
||||
return $scope.catchall.availableMailboxes.find(function (m) { return m.name === name; });
|
||||
}).filter(function (m) { return !!m; });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -61,12 +97,15 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
busy: false,
|
||||
mailinglists: [],
|
||||
search: '',
|
||||
currentPage: 1,
|
||||
perPage: 10,
|
||||
|
||||
add: {
|
||||
busy: false,
|
||||
error: {},
|
||||
name: '',
|
||||
membersTxt: '',
|
||||
membersOnly: false,
|
||||
|
||||
reset: function () {
|
||||
$scope.mailinglists.add.busy = false;
|
||||
@@ -88,7 +127,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
.map(function (m) { return m.trim(); })
|
||||
.filter(function (m) { return m.length !== 0; });
|
||||
|
||||
Client.addMailingList($scope.domain.domain, $scope.mailinglists.add.name, members, function (error) {
|
||||
Client.addMailingList($scope.domain.domain, $scope.mailinglists.add.name, members, $scope.mailinglists.add.membersOnly, function (error) {
|
||||
$scope.mailinglists.add.busy = false;
|
||||
$scope.mailinglists.add.error = {};
|
||||
|
||||
@@ -114,10 +153,14 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
error: {},
|
||||
name: '',
|
||||
membersTxt: '',
|
||||
membersOnly: false,
|
||||
active: true,
|
||||
|
||||
show: function (list) {
|
||||
$scope.mailinglists.edit.name = list.name;
|
||||
$scope.mailinglists.edit.membersTxt = list.members.sort().join('\n');
|
||||
$scope.mailinglists.edit.membersOnly = list.membersOnly;
|
||||
$scope.mailinglists.edit.active = list.active;
|
||||
|
||||
$('#mailinglistEditModal').modal('show');
|
||||
},
|
||||
@@ -129,7 +172,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
.map(function (m) { return m.trim(); })
|
||||
.filter(function (m) { return m.length !== 0; });
|
||||
|
||||
Client.updateMailingList($scope.domain.domain, $scope.mailinglists.edit.name, members, function (error) {
|
||||
Client.updateMailingList($scope.domain.domain, $scope.mailinglists.edit.name, members, $scope.mailinglists.edit.membersOnly, $scope.mailinglists.edit.active, function (error) {
|
||||
$scope.mailinglists.edit.busy = false;
|
||||
$scope.mailinglists.edit.error = {};
|
||||
|
||||
@@ -174,13 +217,29 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
refresh: function (callback) {
|
||||
callback = typeof callback === 'function' ? callback : function (error) { if (error) return console.error(error); };
|
||||
|
||||
Client.listMailingLists($scope.domain.domain, function (error, result) {
|
||||
Client.listMailingLists($scope.domain.domain, $scope.mailinglists.search, $scope.mailinglists.currentPage, $scope.mailinglists.perPage, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
$scope.mailinglists.mailinglists = result;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
showNextPage: function () {
|
||||
$scope.mailinglists.currentPage++;
|
||||
$scope.mailinglists.refresh();
|
||||
},
|
||||
|
||||
showPrevPage: function () {
|
||||
if ($scope.mailinglists.currentPage > 1) $scope.mailinglists.currentPage--;
|
||||
else $scope.mailinglists.currentPage = 1;
|
||||
$scope.mailinglists.refresh();
|
||||
},
|
||||
|
||||
updateFilter: function (fresh) {
|
||||
if (fresh) $scope.mailinglists.currentPage = 1;
|
||||
$scope.mailinglists.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -205,6 +264,29 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
}
|
||||
};
|
||||
|
||||
$scope.banner = {
|
||||
busy: false,
|
||||
text: '',
|
||||
html: '',
|
||||
|
||||
submit: function () {
|
||||
$scope.banner.busy = true;
|
||||
|
||||
Client.setMailBanner($scope.domain.domain, { text: $scope.banner.text, html: $scope.banner.html }, function (error) {
|
||||
if (error) {
|
||||
$scope.banner.busy = false;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
// give sometime for the mail container to restart
|
||||
$timeout(function () {
|
||||
$scope.banner.busy = false;
|
||||
$scope.refreshDomain();
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.incomingEmail = {
|
||||
busy: false,
|
||||
setupDns: true,
|
||||
@@ -221,7 +303,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
setDnsRecords: function (callback) {
|
||||
$scope.incomingEmail.setupDnsBusy = true;
|
||||
|
||||
Client.setDnsRecords($scope.domain.domain, function (error) {
|
||||
Client.setDnsRecords({ domain: $scope.domain.domain, type: 'mail' }, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
$timeout(function () { $scope.incomingEmail.setupDnsBusy = false; }, 2000); // otherwise, it's too fast
|
||||
@@ -245,9 +327,10 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
maybeSetupDns(function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.refreshDomain();
|
||||
|
||||
$scope.incomingEmail.busy = false;
|
||||
$timeout(function () {
|
||||
$scope.refreshDomain();
|
||||
$scope.incomingEmail.busy = false;
|
||||
}, 5000); // wait for mail container to restart. it cannot get IP otherwise while refreshing
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -261,15 +344,186 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.reconfigureEmailApps();
|
||||
$scope.refreshDomain();
|
||||
$scope.incomingEmail.busy = false;
|
||||
|
||||
$timeout(function () {
|
||||
$scope.refreshDomain();
|
||||
$scope.incomingEmail.busy = false;
|
||||
}, 5000); // wait for mail container to restart. it cannot get IP otherwise while refreshing
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.mailboxImport = {
|
||||
busy: false,
|
||||
done: false,
|
||||
error: null,
|
||||
percent: 0,
|
||||
success: 0,
|
||||
mailboxes: [],
|
||||
|
||||
reset: function () {
|
||||
$scope.mailboxImport.busy = false;
|
||||
$scope.mailboxImport.error = null;
|
||||
$scope.mailboxImport.mailboxes = [];
|
||||
$scope.mailboxImport.percent = 0;
|
||||
$scope.mailboxImport.success = 0;
|
||||
$scope.mailboxImport.done = false;
|
||||
},
|
||||
|
||||
handleFileChanged: function () {
|
||||
$scope.mailboxImport.reset();
|
||||
|
||||
var fileInput = document.getElementById('mailboxImportFileInput');
|
||||
if (!fileInput.files || !fileInput.files[0]) return;
|
||||
|
||||
var file = fileInput.files[0];
|
||||
if (file.type !== 'application/json' && file.type !== 'text/csv') return console.log('Unsupported file type.');
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', function () {
|
||||
$scope.$apply(function () {
|
||||
$scope.mailboxImport.mailboxes = [];
|
||||
var mailboxes = [];
|
||||
|
||||
if (file.type === 'text/csv') {
|
||||
var lines = reader.result.split('\n');
|
||||
if (lines.length === 0) return $scope.mailboxImport.error = { file: 'Imported file has no lines' };
|
||||
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
var items = line.split(',');
|
||||
if (items.length !== 4) {
|
||||
$scope.mailboxImport.error = { file: 'Line ' + (i+1) + ' has wrong column count. Expecting 4' };
|
||||
return;
|
||||
}
|
||||
mailboxes.push({
|
||||
name: items[0].trim(),
|
||||
domain: items[1].trim(),
|
||||
owner: items[2].trim(),
|
||||
ownerType: items[3].trim(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
mailboxes = JSON.parse(reader.result).map(function (mailbox) {
|
||||
return {
|
||||
name: mailbox.name,
|
||||
domain: mailbox.domain,
|
||||
owner: mailbox.owner,
|
||||
ownerType: mailbox.ownerType
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to parse mailboxes.', e);
|
||||
$scope.mailboxImport.error = { file: 'Imported file is not valid JSON' };
|
||||
}
|
||||
}
|
||||
|
||||
$scope.mailboxImport.mailboxes = mailboxes;
|
||||
});
|
||||
}, false);
|
||||
reader.readAsText(file);
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.mailboxImport.reset();
|
||||
|
||||
// named so no duplactes
|
||||
document.getElementById('mailboxImportFileInput').addEventListener('change', $scope.mailboxImport.handleFileChanged);
|
||||
|
||||
$('#mailboxImportModal').modal('show');
|
||||
},
|
||||
|
||||
openFileInput: function () {
|
||||
$('#mailboxImportFileInput').click();
|
||||
},
|
||||
|
||||
import: function () {
|
||||
$scope.mailboxImport.percent = 0;
|
||||
$scope.mailboxImport.success = 0;
|
||||
$scope.mailboxImport.done = false;
|
||||
$scope.mailboxImport.error = { import: [] };
|
||||
$scope.mailboxImport.busy = true;
|
||||
|
||||
var processed = 0;
|
||||
|
||||
async.eachSeries($scope.mailboxImport.mailboxes, function (mailbox, callback) {
|
||||
var owner = $scope.owners.find(function (o) { return o.display === mailbox.owner && o.type === mailbox.ownerType; }); // owner may not exist
|
||||
if (!owner) {
|
||||
$scope.mailboxImport.error.import.push({ error: new Error('Could not detect owner'), mailbox: mailbox });
|
||||
++processed;
|
||||
$scope.mailboxImport.percent = 100 * processed / $scope.mailboxImport.mailboxes.length;
|
||||
return callback();
|
||||
}
|
||||
|
||||
Client.addMailbox(mailbox.domain, mailbox.name, owner.id, mailbox.ownerType, function (error) {
|
||||
if (error) $scope.mailboxImport.error.import.push({ error: error, mailbox: mailbox });
|
||||
else ++$scope.mailboxImport.success;
|
||||
|
||||
++processed;
|
||||
$scope.mailboxImport.percent = 100 * processed / $scope.mailboxImport.mailboxes.length;
|
||||
|
||||
callback();
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.mailboxImport.busy = false;
|
||||
$scope.mailboxImport.done = true;
|
||||
if ($scope.mailboxImport.success) $scope.mailboxes.refresh();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.mailboxExport = function (type) {
|
||||
// FIXME only does first 10k mailboxes
|
||||
Client.listMailboxes($scope.domain.domain, '', 1, 10000, function (error, result) {
|
||||
if (error) {
|
||||
Client.error('Failed to list mailboxes. Full error in the webinspector.');
|
||||
return console.error('Failed to list mailboxes.', error);
|
||||
}
|
||||
|
||||
var content = '';
|
||||
|
||||
if (type === 'json') {
|
||||
content = JSON.stringify(result.map(function (mailbox) {
|
||||
var owner = $scope.owners.find(function (o) { return o.id === mailbox.ownerId; }); // owner may not exist
|
||||
|
||||
return {
|
||||
name: mailbox.name,
|
||||
domain: mailbox.domain,
|
||||
owner: owner ? owner.display : '', // this meta property is set when we get the user list
|
||||
ownerType: owner ? owner.type : '',
|
||||
active: mailbox.active,
|
||||
aliases: mailbox.aliases
|
||||
};
|
||||
}), null, 2);
|
||||
} else if (type === 'csv') {
|
||||
content = result.map(function (mailbox) {
|
||||
var owner = $scope.owners.find(function (o) { return o.id === mailbox.ownerId; }); // owner may not exist
|
||||
|
||||
var aliases = mailbox.aliases.map(function (a) { return a.name + '@' + a.domain; }).join(' ');
|
||||
return [ mailbox.name, mailbox.domain, owner ? owner.display : '', owner ? owner.type : '', aliases, mailbox.active ].join(',');
|
||||
}).join('\n');
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
var file = new Blob([ content ], { type: type === 'json' ? 'application/json' : 'text/csv' });
|
||||
var a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(file);
|
||||
a.download = $scope.domain.domain.replaceAll('.','_') + '-mailboxes.' + type;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.mailboxes = {
|
||||
mailboxes: [],
|
||||
search: '',
|
||||
currentPage: 1,
|
||||
perPage: 10,
|
||||
|
||||
add: {
|
||||
error: null,
|
||||
@@ -285,6 +539,11 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
},
|
||||
|
||||
show: function () {
|
||||
if ($scope.config.features.mailboxMaxCount && $scope.config.features.mailboxMaxCount <= $scope.mailboxes.mailboxes.length) {
|
||||
$('#subscriptionRequiredModal').modal('show');
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.mailboxes.add.reset();
|
||||
$('#mailboxAddModal').modal('show');
|
||||
},
|
||||
@@ -292,21 +551,17 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
submit: function () {
|
||||
$scope.mailboxes.add.busy = true;
|
||||
|
||||
Client.addMailbox($scope.domain.domain, $scope.mailboxes.add.name, $scope.mailboxes.add.owner.id, function (error) {
|
||||
Client.addMailbox($scope.domain.domain, $scope.mailboxes.add.name, $scope.mailboxes.add.owner.id, $scope.mailboxes.add.owner.type, function (error) {
|
||||
if (error) {
|
||||
$scope.mailboxes.add.busy = false;
|
||||
$scope.mailboxes.add.error = error;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.mailboxes.add.reset();
|
||||
$scope.mailboxes.refresh(function (error) {
|
||||
if (error) return console.error(error);
|
||||
$scope.mailboxes.refresh();
|
||||
$scope.catchall.refresh();
|
||||
|
||||
$scope.catchall.refresh();
|
||||
|
||||
$('#mailboxAddModal').modal('hide');
|
||||
});
|
||||
$('#mailboxAddModal').modal('hide');
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -316,12 +571,32 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
error: null,
|
||||
name: '',
|
||||
owner: null,
|
||||
aliases: '',
|
||||
incomingDomains: [],
|
||||
aliases: [],
|
||||
active: true,
|
||||
enablePop3: false,
|
||||
|
||||
addAlias: function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
$scope.mailboxes.edit.aliases.push({
|
||||
name: '',
|
||||
domain: domainName,
|
||||
reversedSortingNotation: 'z'.repeat(100) // quick and dirty to ensure newly added are on bottom
|
||||
});
|
||||
},
|
||||
|
||||
delAlias: function (event, index) {
|
||||
event.preventDefault();
|
||||
$scope.mailboxes.edit.aliases.splice(index, 1);
|
||||
},
|
||||
|
||||
show: function (mailbox) {
|
||||
$scope.mailboxes.edit.name = mailbox.name;
|
||||
$scope.mailboxes.edit.owner = mailbox.owner; // this can be null if mailbox had no owner
|
||||
$scope.mailboxes.edit.aliases = mailbox.aliases;
|
||||
$scope.mailboxes.edit.aliases = angular.copy(mailbox.aliases, []).map(function (a) { a.reversedSortingNotation = a.domain + '@' + a.name; return a; });
|
||||
$scope.mailboxes.edit.active = mailbox.active;
|
||||
$scope.mailboxes.edit.enablePop3 = mailbox.enablePop3;
|
||||
|
||||
$('#mailboxEditModal').modal('show');
|
||||
},
|
||||
@@ -329,17 +604,22 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
submit: function () {
|
||||
$scope.mailboxes.edit.busy = true;
|
||||
|
||||
var data = {
|
||||
ownerId: $scope.mailboxes.edit.owner.id,
|
||||
ownerType: $scope.mailboxes.edit.owner.type,
|
||||
active: $scope.mailboxes.edit.active,
|
||||
enablePop3: $scope.mailboxes.edit.enablePop3
|
||||
};
|
||||
|
||||
// $scope.mailboxes.edit.owner is expected to be validated by the UI
|
||||
Client.updateMailbox($scope.domain.domain, $scope.mailboxes.edit.name, $scope.mailboxes.edit.owner.id, function (error) {
|
||||
Client.updateMailbox($scope.domain.domain, $scope.mailboxes.edit.name, data, function (error) {
|
||||
if (error) {
|
||||
$scope.mailboxes.edit.error = error;
|
||||
$scope.mailboxes.edit.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var aliases = $scope.mailboxes.edit.aliases.split(',').map(function (a) { return a.trim(); }).filter(function (a) { return !!a; });
|
||||
|
||||
Client.setAliases($scope.domain.domain, $scope.mailboxes.edit.name, aliases, function (error) {
|
||||
Client.setAliases($scope.mailboxes.edit.name, $scope.domain.domain, $scope.mailboxes.edit.aliases, function (error) {
|
||||
if (error) {
|
||||
$scope.mailboxes.edit.error = error;
|
||||
$scope.mailboxes.edit.busy = false;
|
||||
@@ -350,7 +630,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
$scope.mailboxes.edit.error = null;
|
||||
$scope.mailboxes.edit.name = '';
|
||||
$scope.mailboxes.edit.owner = null;
|
||||
$scope.mailboxes.edit.aliases = '';
|
||||
$scope.mailboxes.edit.aliases = [];
|
||||
$scope.mailboxes.refresh();
|
||||
|
||||
$('#mailboxEditModal').modal('hide');
|
||||
@@ -362,6 +642,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
remove: {
|
||||
busy: false,
|
||||
mailbox: null,
|
||||
deleteMails: true,
|
||||
|
||||
show: function (mailbox) {
|
||||
$scope.mailboxes.remove.mailbox = mailbox;
|
||||
@@ -372,7 +653,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
submit: function () {
|
||||
$scope.mailboxes.remove.busy = true;
|
||||
|
||||
Client.removeMailbox($scope.domain.domain, $scope.mailboxes.remove.mailbox.name, function (error) {
|
||||
Client.removeMailbox($scope.domain.domain, $scope.mailboxes.remove.mailbox.name, $scope.mailboxes.remove.deleteMails, function (error) {
|
||||
$scope.mailboxes.remove.busy = false;
|
||||
if (error) return console.error(error);
|
||||
|
||||
@@ -391,32 +672,37 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
refresh: function (callback) {
|
||||
callback = typeof callback === 'function' ? callback : function (error) { if (error) return console.error(error); };
|
||||
|
||||
Client.getMailboxes($scope.domain.domain, function (error, mailboxes) {
|
||||
Client.listMailboxes($scope.domain.domain, $scope.mailboxes.search, $scope.mailboxes.currentPage, $scope.mailboxes.perPage, function (error, mailboxes) {
|
||||
if (error) return callback(error);
|
||||
|
||||
Client.listAliases($scope.domain.domain, function (error, aliases) {
|
||||
if (error) return callback(error);
|
||||
mailboxes.forEach(function (m) {
|
||||
m.owner = $scope.owners.find(function (o) { return o.id === m.ownerId; }); // owner may not exist
|
||||
m.ownerDisplayName = m.owner ? m.owner.display : ''; // this meta property is set when we get the user list
|
||||
|
||||
$scope.mailboxes.mailboxes = mailboxes.map(function (m) {
|
||||
m.aliases = aliases.filter(function (a) { return a.aliasTarget === m.name; }).map(function (a) { return a.name; }).join(',');
|
||||
m.owner = $scope.users.find(function (u) { return u.id === m.ownerId; }); // owner may not exist
|
||||
m.ownerDisplayName = m.owner ? m.owner.display : ''; // this meta property is set when we get the user list
|
||||
|
||||
return m;
|
||||
});
|
||||
|
||||
Client.getMailUsage($scope.domain.domain, function (error, usage) {
|
||||
if (error) return callback(error);
|
||||
|
||||
$scope.mailboxes.mailboxes.forEach(function (m) {
|
||||
var u = usage[m.name + '@' + m.domain]; // this is unset when no emails have been received yet
|
||||
m.usage = (u && u.size) || 0;
|
||||
});
|
||||
|
||||
callback();
|
||||
});
|
||||
var u = $scope.diskUsage[m.name + '@' + m.domain]; // this is unset when no emails have been received yet
|
||||
m.usage = (u && u.size) || 0;
|
||||
});
|
||||
|
||||
$scope.mailboxes.mailboxes = mailboxes;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
showNextPage: function () {
|
||||
$scope.mailboxes.currentPage++;
|
||||
$scope.mailboxes.refresh();
|
||||
},
|
||||
|
||||
showPrevPage: function () {
|
||||
if ($scope.mailboxes.currentPage > 1) $scope.mailboxes.currentPage--;
|
||||
else $scope.mailboxes.currentPage = 1;
|
||||
$scope.mailboxes.refresh();
|
||||
},
|
||||
|
||||
updateFilter: function (fresh) {
|
||||
if (fresh) $scope.mailboxes.currentPage = 1;
|
||||
$scope.mailboxes.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -425,6 +711,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
{ provider: 'external-smtp', name: 'External SMTP server', host: '', port: 587 },
|
||||
{ provider: 'external-smtp-noauth', name: 'External SMTP server (No Authentication)', host: '', port: 587 },
|
||||
{ provider: 'ses-smtp', name: 'Amazon SES', host: 'email-smtp.us-east-1.amazonaws.com', port: 587, spfDoc: 'https://docs.aws.amazon.com/ses/latest/DeveloperGuide/spf.html' },
|
||||
{ provider: 'elasticemail-smtp', name: 'Elastic Email', host: 'smtp.elasticemail.com', port: 587, spfDoc: 'https://elasticemail.com/blog/marketing_tips/common-spf-errors' },
|
||||
{ provider: 'google-smtp', name: 'Google', host: 'smtp.gmail.com', port: 587, spfDoc: 'https://support.google.com/a/answer/33786?hl=en' },
|
||||
{ provider: 'mailgun-smtp', name: 'Mailgun', host: 'smtp.mailgun.org', port: 587, spfDoc: 'https://www.mailgun.com/blog/white-labeling-dns-records-your-customers-tips-tricks' },
|
||||
{ provider: 'mailjet-smtp', name: 'Mailjet', host: '', port: 587, spfDoc: 'https://app.mailjet.com/docs/spf-dkim-guide' },
|
||||
@@ -447,6 +734,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
|| provider === 'ses-smtp'
|
||||
|| provider === 'google-smtp'
|
||||
|| provider === 'mailgun-smtp'
|
||||
|| provider === 'elasticemail-smtp'
|
||||
|| provider === 'mailjet-smtp';
|
||||
};
|
||||
|
||||
@@ -456,6 +744,17 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
busy: false,
|
||||
preset: $scope.mailRelayPresets[0],
|
||||
|
||||
// form data to be set on load
|
||||
relay: {
|
||||
provider: 'cloudron-smtp',
|
||||
host: '',
|
||||
port: 25,
|
||||
username: '',
|
||||
password: '',
|
||||
serverApiToken: '',
|
||||
acceptSelfSignedCerts: false
|
||||
},
|
||||
|
||||
presetChanged: function () {
|
||||
$scope.mailRelay.error = null;
|
||||
|
||||
@@ -468,17 +767,6 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
$scope.mailRelay.relay.acceptSelfSignedCerts = false;
|
||||
},
|
||||
|
||||
// form data to be set on load
|
||||
relay: {
|
||||
provider: 'cloudron-smtp',
|
||||
host: '',
|
||||
port: 25,
|
||||
username: '',
|
||||
password: '',
|
||||
serverApiToken: '',
|
||||
acceptSelfSignedCerts: false
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.mailRelay.error = null;
|
||||
$scope.mailRelay.busy = true;
|
||||
@@ -488,13 +776,15 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
provider: $scope.mailRelay.relay.provider,
|
||||
host: $scope.mailRelay.relay.host,
|
||||
port: $scope.mailRelay.relay.port,
|
||||
acceptSelfSignedCerts: $scope.mailRelay.relay.acceptSelfSignedCerts
|
||||
acceptSelfSignedCerts: $scope.mailRelay.relay.acceptSelfSignedCerts,
|
||||
forceFromAddress: false
|
||||
};
|
||||
|
||||
// fill in provider specific username/password usage
|
||||
if (data.provider === 'postmark-smtp') {
|
||||
data.username = $scope.mailRelay.relay.serverApiToken;
|
||||
data.password = $scope.mailRelay.relay.serverApiToken;
|
||||
data.forceFromAddress = true; // postmark requires the "From:" in mail to be a Sender Signature
|
||||
} else if (data.provider === 'sendgrid-smtp') {
|
||||
data.username = 'apikey';
|
||||
data.password = $scope.mailRelay.relay.serverApiToken;
|
||||
@@ -566,7 +856,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
$location.path('/email/' + $scope.domain.domain, false);
|
||||
};
|
||||
|
||||
// this is required because we need to rewrite the MAIL_DOMAINS env var
|
||||
// this is required because we need to rewrite the CLOUDRON_MAIL_SERVER_HOST env var
|
||||
$scope.reconfigureEmailApps = function () {
|
||||
var installedApps = Client.getInstalledApps();
|
||||
for (var i = 0; i < installedApps.length; i++) {
|
||||
@@ -584,6 +874,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
resetDnsRecords();
|
||||
|
||||
Client.getMailConfigForDomain(domainName, function (error, mailConfig) {
|
||||
if (error && error.statusCode === 404) return $location.path('/email');
|
||||
if (error) {
|
||||
$scope.refreshBusy = false;
|
||||
return console.error(error);
|
||||
@@ -616,13 +907,19 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
}
|
||||
}
|
||||
|
||||
$scope.banner.text = mailConfig.banner.text || '';
|
||||
$scope.banner.html = mailConfig.banner.html || '';
|
||||
|
||||
// amend to selected domain to be available for the UI
|
||||
$scope.domain.mailConfig = mailConfig;
|
||||
$scope.domain.mailStatus = {};
|
||||
|
||||
$scope.mailboxes.refresh(function (error) {
|
||||
Client.getMailUsage($scope.domain.domain, function (error, usage) {
|
||||
if (error) console.error(error);
|
||||
|
||||
$scope.diskUsage = usage || {}; // if mail server is down, don't stop the listing
|
||||
|
||||
$scope.mailboxes.refresh(); // relies on disk usage
|
||||
$scope.mailinglists.refresh();
|
||||
$scope.catchall.refresh();
|
||||
});
|
||||
@@ -658,33 +955,57 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
});
|
||||
};
|
||||
|
||||
$scope.howToConnectInfo = {
|
||||
show: function () {
|
||||
$('#howToConnectInfoModal').modal('show');
|
||||
}
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
$scope.isAdminDomain = $scope.config.adminDomain === domainName;
|
||||
|
||||
Client.getUsers(function (error, users) {
|
||||
Client.getAllUsers(function (error, users) {
|
||||
if (error) return console.error('Unable to get user listing.', error);
|
||||
|
||||
// ensure we have a display value available
|
||||
$scope.users = users.map(function (u) {
|
||||
u.display = u.username || u.email;
|
||||
return u;
|
||||
$scope.owners.push({ header: true, display: $translate.instant('email.mailboxboxDialog.usersHeader') });
|
||||
users.forEach(function (u) {
|
||||
$scope.owners.push({ display: u.username || u.email, id: u.id, type: 'user' });
|
||||
});
|
||||
|
||||
$scope.users = users;
|
||||
Client.getGroups(function (error, groups) {
|
||||
if (error) return console.error('Unable to get group listing.', error);
|
||||
|
||||
Client.getDomain(domainName, function (error, result) {
|
||||
if (error) return console.error('Unable to get view domain.', error);
|
||||
$scope.owners.push({ header: true, display: $translate.instant('email.mailboxboxDialog.groupsHeader') });
|
||||
groups.forEach(function (g) {
|
||||
$scope.owners.push({ display: g.name, id: g.id, type: 'group' });
|
||||
});
|
||||
|
||||
$scope.domain = result;
|
||||
$scope.owners.push({ header: true, display: $translate.instant('email.mailboxboxDialog.appsHeader') });
|
||||
Client.getInstalledApps().forEach(function (a) {
|
||||
if (a.manifest.addons && a.manifest.addons.recvmail) $scope.owners.push({ display: a.label || a.fqdn, id: a.id, type: 'app' });
|
||||
});
|
||||
|
||||
Client.getDomain($scope.config.adminDomain, function (error, result) {
|
||||
if (error) return console.error('Unable to get admin domain.', error);
|
||||
|
||||
$scope.adminDomain = result;
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) return console.error('Unable to list domains.', error);
|
||||
|
||||
$scope.domain = result.filter(function (d) { return d.domain === domainName; })[0];
|
||||
$scope.adminDomain = result.filter(function (d) { return d.domain === $scope.config.adminDomain; })[0];
|
||||
$scope.refreshDomain();
|
||||
|
||||
$scope.ready = true;
|
||||
async.eachSeries(result, function (domain, iteratorDone) {
|
||||
Client.getMailConfigForDomain(domain.domain, function (error, mailConfig) {
|
||||
if (error) return console.error('Failed to fetch mail config for domain', domain.domain, error);
|
||||
|
||||
if (mailConfig.enabled) $scope.incomingDomains.push(domain);
|
||||
iteratorDone();
|
||||
});
|
||||
}, function iteratorDone(error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.setView($routeParams.view || 'mailboxes', true /* always set */);
|
||||
$scope.ready = true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<div>
|
||||
<a href="/#/email" class="back-to-view-link"><i class="fas fa-arrow-left"></i> {{ 'email.backAction' | tr }}</a>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<h1>
|
||||
{{ 'emails.eventlog.title' | tr }}
|
||||
|
||||
<a class="btn btn-default btn-outline pull-right" href="/logs.html?id=mail" target="_blank">{{ 'main.action.logs' | tr }}</a>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="maillog-filter">
|
||||
<input class="form-control" style="width: 200px;" placeholder="{{ 'main.searchPlaceholder' | tr }}" type="text" ng-model="activity.search" ng-model-options="{ debounce: 1000 }" ng-change="activity.updateFilter(true)" />
|
||||
<multiselect ng-model="activity.selectedTypes" ms-header="{{ 'emails.typeFilterHeader' | tr }}" options="a.name for a in activityTypes" data-multiple="true" ng-change="activity.updateFilter(true)" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<select class="form-control" ng-model="activity.pageItems" ng-options="a.name for a in pageItemCount" ng-change="activity.updateFilter(true)"></select>
|
||||
</div>
|
||||
<div class="pagination pull-right">
|
||||
<button class="btn btn-default btn-outline" ng-click="activity.refresh()"><i class="fas fa-sync-alt" ng-class="{ 'fa-spin': busyRefresh }"></i></button>
|
||||
<button class="btn btn-default btn-outline" ng-click="activity.showPrevPage()" ng-disabled="activity.busy || activity.currentPage <= 1"><i class="fa fa-angle-double-left"></i> {{ 'main.pagination.prev' | tr }}</button>
|
||||
<button class="btn btn-default btn-outline" ng-click="activity.showNextPage()" ng-disabled="activity.busy || activity.perPage > activity.eventLogs.length">{{ 'main.pagination.next' | tr }} <i class="fa fa-angle-double-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="card card-block" style="max-width: 100%">
|
||||
<div>
|
||||
<center ng-show="activity.busy"><h2><i class="fa fa-circle-notch fa-spin"></i></h2></center>
|
||||
<table ng-hide="activity.busy" class="table table-hover" style="margin: 0; table-layout:fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 5%"><!-- Icon --></th>
|
||||
<th style="width: 15%">{{ 'emails.eventlog.time' | tr }}</th>
|
||||
<th style="width: 25%">{{ 'emails.eventlog.mailFrom' | tr }}</th>
|
||||
<th style="width: 25%">{{ 'emails.eventlog.rcptTo' | tr }}</th>
|
||||
<th style="width: 30%">{{ 'emails.eventlog.details' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody ng-hide="activity.eventLogs.length">
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">
|
||||
<br>
|
||||
<br>
|
||||
{{ 'emails.eventlog.empty' | tr }}
|
||||
<br>
|
||||
<br>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody ng-show="activity.eventLogs.length" ng-repeat="eventlog in activity.eventLogs">
|
||||
<tr ng-click="activity.showEventLogDetails(eventlog)" class="hand">
|
||||
<td class="no-wrap">
|
||||
<i class="fas fa-arrow-circle-left" ng-show="eventlog.type === 'delivered'" uib-tooltip="{{ 'emails.eventlog.type.outgoing' | tr }}"></i>
|
||||
<i class="fas fa-history" ng-show="eventlog.type === 'deferred'" uib-tooltip="{{ 'emails.eventlog.type.deferred' | tr }}"></i>
|
||||
<i class="fas fa-arrow-circle-right" ng-show="eventlog.type === 'received'" uib-tooltip="{{ 'emails.eventlog.type.incoming' | tr }}"></i>
|
||||
<i class="fas fa-align-justify" ng-show="eventlog.type === 'queued' && eventlog.spamStatus.indexOf('Yes,') !== 0" uib-tooltip="{{ 'emails.eventlog.type.queued' | tr }}"></i>
|
||||
<i class="fas fa-trash" ng-show="eventlog.type === 'queued' && eventlog.spamStatus.indexOf('Yes,') === 0" uib-tooltip="{{ 'emails.eventlog.type.queued' | tr }}"></i>
|
||||
<i class="fas fa-minus-circle" ng-show="eventlog.type === 'denied'" uib-tooltip="{{ 'emails.eventlog.type.denied' | tr }}"></i>
|
||||
<i class="fas fa-hand-paper" ng-show="eventlog.type === 'bounce'" uib-tooltip="{{ 'emails.eventlog.type.bounce' | tr }}"></i>
|
||||
<i class="fas fa-filter" ng-show="eventlog.type === 'spam-learn'" uib-tooltip="{{ 'emails.eventlog.type.spamFilterTrained' | tr }}"></i>
|
||||
</td>
|
||||
<td class="no-wrap"><span uib-tooltip="{{ eventlog.ts | prettyLongDate }}" class="arrow">{{ eventlog.ts | prettyDate }}</span></td>
|
||||
<td class="elide-table-cell">{{ (eventlog.mailFrom | prettyEmailAddresses) || '-' }}</td>
|
||||
<td class="elide-table-cell">{{ (eventlog.rcptTo | prettyEmailAddresses) || '-' }}</td>
|
||||
<td>
|
||||
<span ng-show="eventlog.type === 'bounce'">{{ 'emails.eventlog.type.bounceInfo' | tr }}. {{ eventlog.message || eventlog.reason }}</span>
|
||||
<span ng-show="eventlog.type === 'deferred'">{{ 'emails.eventlog.type.deferredInfo' | tr: { delay:eventlog.delay } }}. {{ eventlog.message || eventlog.reason }} </span>
|
||||
<span ng-show="eventlog.type === 'queued'">
|
||||
<span ng-show="eventlog.direction === 'inbound'">{{ 'emails.eventlog.type.inboundInfo' | tr }}</span>
|
||||
<span ng-show="eventlog.direction === 'outbound'">{{ 'emails.eventlog.type.outboundInfo' | tr }}</span>
|
||||
</span>
|
||||
<span ng-show="eventlog.type === 'received'">{{ 'emails.eventlog.type.receivedInfo' | tr }}</span>
|
||||
<span ng-show="eventlog.type === 'delivered'">{{ 'emails.eventlog.type.deliveredInfo' | tr }}</span>
|
||||
<span ng-show="eventlog.type === 'denied'">{{ 'emails.eventlog.type.deniedInfo' | tr }}. {{ eventlog.message || eventlog.reason }} </span>
|
||||
<span ng-show="eventlog.type === 'spam-learn'">{{ 'emails.eventlog.type.spamFilterTrainedInfo' | tr }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="activity.activeEventLog === eventlog">
|
||||
<td colspan="6">
|
||||
<pre class="eventlog-details">{{ eventlog | json }}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,82 @@
|
||||
'use strict';
|
||||
|
||||
/* global $ */
|
||||
/* 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('/'); });
|
||||
|
||||
$scope.ready = false;
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
|
||||
$scope.pageItemCount = [
|
||||
{ name: $translate.instant('main.pagination.perPageSelector', { n: 20 }), value: 20 },
|
||||
{ name: $translate.instant('main.pagination.perPageSelector', { n: 50 }), value: 50 },
|
||||
{ name: $translate.instant('main.pagination.perPageSelector', { n: 100 }), value: 100 }
|
||||
];
|
||||
|
||||
$scope.activityTypes = [
|
||||
{ name: 'Bounce', value: 'bounce' },
|
||||
{ name: 'Deferred', value: 'deferred' },
|
||||
{ name: 'Delivered', value: 'delivered' },
|
||||
{ name: 'Denied', value: 'denied' },
|
||||
{ name: 'Queued', value: 'queued' },
|
||||
{ name: 'Received', value: 'received' },
|
||||
{ name: 'Spam', value: 'spam' },
|
||||
];
|
||||
|
||||
$scope.activity = {
|
||||
busy: true,
|
||||
eventLogs: [],
|
||||
activeEventLog: null,
|
||||
currentPage: 1,
|
||||
perPage: 20,
|
||||
pageItems: $scope.pageItemCount[0],
|
||||
selectedTypes: [],
|
||||
search: '',
|
||||
|
||||
refresh: function () {
|
||||
$scope.activity.busy = true;
|
||||
|
||||
var types = $scope.activity.selectedTypes.map(function (a) { return a.value; }).join(',');
|
||||
|
||||
Client.getMailEventLogs($scope.activity.search, types, $scope.activity.currentPage, $scope.activity.pageItems.value, function (error, result) {
|
||||
if (error) return console.error('Failed to fetch mail eventlogs.', error);
|
||||
|
||||
$scope.activity.busy = false;
|
||||
|
||||
$scope.activity.eventLogs = result;
|
||||
});
|
||||
},
|
||||
|
||||
showNextPage: function () {
|
||||
$scope.activity.currentPage++;
|
||||
$scope.activity.refresh();
|
||||
},
|
||||
|
||||
showPrevPage: function () {
|
||||
if ($scope.activity.currentPage > 1) $scope.activity.currentPage--;
|
||||
else $scope.activity.currentPage = 1;
|
||||
$scope.activity.refresh();
|
||||
},
|
||||
|
||||
showEventLogDetails: function (eventLog) {
|
||||
if ($scope.activity.activeEventLog === eventLog) $scope.activity.activeEventLog = null;
|
||||
else $scope.activity.activeEventLog = eventLog;
|
||||
},
|
||||
|
||||
updateFilter: function (fresh) {
|
||||
if (fresh) $scope.activity.currentPage = 1;
|
||||
$scope.activity.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
$scope.ready = true;
|
||||
|
||||
$scope.activity.refresh();
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
+277
-93
@@ -1,21 +1,199 @@
|
||||
<!-- Modal change mail server domain -->
|
||||
<div class="modal fade" id="mailLocationModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'emails.changeDomainDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-bind-html=" 'emails.changeDomainDialog.description' | tr "></div>
|
||||
<br>
|
||||
|
||||
<form name="mailLocationForm" role="form" novalidate ng-submit="mailLocation.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (mailLocationForm.subdomain.$dirty && mailLocationForm.subdomain.$invalid) || (!mailLocationForm.subdomain.$dirty && mailLocation.error)}">
|
||||
<label class="control-label">{{ 'emails.changeDomainDialog.location' | tr }}</label>
|
||||
|
||||
<div class="has-error" ng-show="mailLocation.error">{{ mailLocation.error.message }}</div>
|
||||
|
||||
<div class="input-group form-inline">
|
||||
<input type="text" class="form-control" ng-model="mailLocation.subdomain" id="mailLocationLocationInput" name="location" placeholder="{{ 'emails.changeDomainDialog.locationPlaceholder' | tr }}" autofocus>
|
||||
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<span>{{ (!mailLocation.subdomain ? '' : '.') + mailLocation.domain.domain }}</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li ng-repeat="domain in domains">
|
||||
<a href="" ng-click="mailLocation.domain = domain">{{ domain.domain }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-warning text-bold" ng-show="mailLocation.domain.provider === 'manual'" ng-bind-html="'emails.changeDomainDialog.manualInfo' | tr:{ domain: mailLocation.domain.domain }"></p>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="mailLocationForm.$invalid"/>
|
||||
</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="mailLocation.submit()" ng-disabled="mailLocationForm.$invalid || mailLocation.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailLocation.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change max email size -->
|
||||
<div class="modal fade" id="maxEmailSizeChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'emails.changeMailSizeDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-bind-html=" 'emails.changeMailSizeDialog.description' | tr "></div>
|
||||
<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 | prettyDiskSize }}</b></label>
|
||||
<slider ng-model="maxEmailSize.size" tooltip="hide" min="1048576" max="1073741824" step="1048576"></slider>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit"/>
|
||||
</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="maxEmailSize.submit()" ng-disabled="maxEmailSize.size === maxEmailSize.currentSize"><i class="fa fa-circle-notch fa-spin" ng-show="maxEmailSize.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change mailbox sharing -->
|
||||
<div class="modal fade" id="mailboxSharingChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'emails.mailboxSharingDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-bind-html=" 'emails.mailboxSharingDialog.description' | tr "></div>
|
||||
<br>
|
||||
<form name="mailboxSharingChangeForm" role="form" novalidate ng-submit="mailboxSharing.submit()" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailboxSharing.enable">{{ 'emails.mailboxSharing.mailboxSharingCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit"/>
|
||||
</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="mailboxSharing.submit()" ng-disabled="mailboxSharing.enable === mailboxSharing.enabled"><i class="fa fa-circle-notch fa-spin" ng-show="mailboxSharing.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal solr config -->
|
||||
<div class="modal fade" id="solrConfigModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'emails.solrConfig.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html=" 'emails.solrConfig.description' | tr "></p>
|
||||
<!-- only show this when user is trying to enable -->
|
||||
<p class="has-error" ng-show="!solrConfig.currentConfig.enabled && !solrConfig.enoughMemory">{{ 'emails.solrConfig.notEnoughMemory' | tr }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-hide="solrConfig.currentConfig.enabled" ng-click="solrConfig.submit(true)" ng-disabled="(!solrConfig.currentConfig.enabled && !solrConfig.enoughMemory) || solrConfig.busy"><i class="fa fa-circle-notch fa-spin" ng-show="solrConfig.busy"></i> {{ 'main.enableAction' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-show="solrConfig.currentConfig.enabled" ng-click="solrConfig.submit(false)" ng-disabled="solrConfig.busy"><i class="fa fa-circle-notch fa-spin" ng-show="solrConfig.busy"></i> {{ 'main.disableAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change acl -->
|
||||
<div class="modal fade" id="aclChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'emails.aclDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="aclChangeForm" role="form" novalidate ng-submit="acl.submit()" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'emails.aclDialog.dnsblZones' | tr }} <sup><a ng-href="https://docs.cloudron.io/email/#dnsbl" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<p class="small">{{ 'emails.aclDialog.dnsblZonesInfo' | tr }}</p>
|
||||
<div class="has-error" ng-show="acl.error.dnsblZones">{{ acl.error.dnsblZones }}</div>
|
||||
<textarea ng-model="acl.dnsblZones" placeholder="{{ 'emails.aclDialog.dnsblZonesPlaceholder' | tr }}" name="dnsblZones" class="form-control" ng-class="{ 'has-error': !aclChangeForm.dnsblZones.$dirty && acl.error.dnsblZones }" rows="4"></textarea>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit"/>
|
||||
</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="acl.submit()"><i class="fa fa-circle-notch fa-spin" ng-show="acl.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change spam config -->
|
||||
<div class="modal fade" id="spamConfigChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'emails.spamFilterDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="spamConfigChangeForm" role="form" novalidate ng-submit="spamConfig.submit()" autocomplete="off">
|
||||
<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>
|
||||
<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>
|
||||
<div class="has-error" ng-show="spamConfig.error.config">{{ spamConfig.error.config }}</div>
|
||||
<textarea ng-model="spamConfig.config" placeholder="{{ 'emails.spamFilterDialog.customRulesPlaceholder' | tr }}" class="form-control" name="config" ng-class="{ 'has-error': !spamConfigChangeForm.config.$dirty && spamConfig.error.config }" rows="4"></textarea>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit"/>
|
||||
</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="spamConfig.submit()"><i class="fa fa-circle-notch fa-spin" ng-show="spamConfig.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test email -->
|
||||
<div class="modal fade" id="testEmailModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Send test email for <b>{{ testEmail.domain.domain }}</b></h4>
|
||||
<h4 class="modal-title">{{ 'emails.testMailDialog.title' | tr:{ domain: testEmail.domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="testEmailForm" role="form" novalidate ng-submit="testEmail.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<p class="has-error text-center" ng-show="testEmail.error">{{ testEmail.error.generic }}</p>
|
||||
|
||||
<p>This will send a test email from <b>no-reply@{{testEmail.domain.domain}}</b> to the address below.</p>
|
||||
<p ng-bind-html="'emails.testMailDialog.description' | tr:{ domain: testEmail.domain.domain }"></p>
|
||||
<br/>
|
||||
<div class="form-group" ng-class="{ 'has-error': testEmail.error.key }">
|
||||
<label class="control-label" for="inputTestEmailKey">Email to</label>
|
||||
<input type="text" class="form-control" ng-model="testEmail.mailTo" id="inputTestMailTo" name="mailTo" ng-disabled="testEmail.busy" placeholder="Email address" autofocus>
|
||||
<label class="control-label" for="inputTestEmailKey">{{ 'emails.testMailDialog.mailTo' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="testEmail.mailTo" id="inputTestMailTo" name="mailTo" ng-disabled="testEmail.busy" placeholder="{{ 'emails.testMailDialog.mailToPlaceholder' | tr }}" autofocus>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="testEmailForm.$invalid"/>
|
||||
@@ -23,10 +201,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="testEmail.submit()" ng-disabled="testEmail.$invalid || testEmail.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="testEmail.busy"></i><span>Send</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="testEmail.submit()" ng-disabled="testEmail.$invalid || testEmail.busy"><i class="fa fa-circle-notch fa-spin" ng-show="testEmail.busy"></i> {{ 'emails.testMailDialog.sendAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,15 +211,21 @@
|
||||
<div class="content">
|
||||
<div class="text-left">
|
||||
<h1>
|
||||
Mail Server
|
||||
{{ 'emails.title' | tr }}
|
||||
|
||||
<div class="btn-group pull-right" role="group">
|
||||
<a class="btn btn-default" href="#/emails-eventlog">{{ 'main.action.logs' | tr }}</a>
|
||||
<!-- hidden for now, until we see a purpose -->
|
||||
<!-- <a class="btn btn-sm btn-default" ng-disabled="user.role !== 'owner'" href="/filemanager.html?id=mail&type=mail" target="_blank" uib-tooltip="{{ 'app.filemanagerActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fas fa-folder"></i></a> -->
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>Domains</h3>
|
||||
<h3>{{ 'emails.domains.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row ng-hide" ng-hide="ready">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
@@ -55,15 +237,15 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 5%"></th>
|
||||
<th style="width: 30%">Domain</th>
|
||||
<th style="width: 60%">Config</th>
|
||||
<th style="width: 10%">Actions</th>
|
||||
<th style="width: 30%">{{ 'emails.domains.domain' | tr }}</th>
|
||||
<th style="width: 60%">{{ 'emails.domains.config' | tr }}</th>
|
||||
<th style="width: 10%">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="domain in domains">
|
||||
<td>
|
||||
<i class="fa fa-circle" ng-style="{ color: domain.statusOk ? '#27CE65' : '#d9534f' }" ng-show="domain.status"></i>
|
||||
<i class="fa fa-circle" ng-class="{ 'status-active': domain.statusOk, 'status-error': !domain.statusOk }" ng-show="domain.status"></i>
|
||||
<i class="fa fa-circle-notch fa-spin" ng-hide="domain.status"></i>
|
||||
</td>
|
||||
<td class="elide-table-cell no-padding">
|
||||
@@ -71,15 +253,13 @@
|
||||
</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.mailboxCount }} Mailbox(es) / Usage: {{ domain.usage | prettyMailSize }}
|
||||
</span>
|
||||
<span ng-show="!domain.inbound && domain.outbound">Outbound only</span>
|
||||
<span ng-show="!domain.inbound && !domain.outbound">Disabled</span>
|
||||
<span ng-show="domain.inbound && domain.outbound">{{ 'emails.domains.stats' | tr:{ mailboxCount: domain.mailboxCount, usage: (domain.usage | prettyByteSize) } }}</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>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right no-wrap">
|
||||
<button class="btn btn-xs btn-default" ng-click="testEmail.show(domain)" uib-tooltip="Send Test Email"><i class="fa fa-paper-plane"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="testEmail.show(domain)" uib-tooltip="{{ 'emails.domains.testEmailTooltip' | tr }}"><i class="fa fa-paper-plane"></i></button>
|
||||
<a href="/#/email/{{ domain.domain }}" class="btn btn-xs btn-default"><i class="fa fa-pencil-alt"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -89,83 +269,87 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>Event Log
|
||||
|
||||
<button class="btn btn-sm btn-default btn-outline pull-right" ng-click="activity.showNextPage()" ng-disabled="activity.busy || activity.perPage > activity.eventLogs.length">next <i class="fa fa-angle-double-right"></i></button>
|
||||
<button class="btn btn-sm btn-default btn-outline pull-right" ng-click="activity.showPrevPage()" ng-disabled="activity.busy || activity.currentPage <= 1"><i class="fa fa-angle-double-left"></i> prev</button>
|
||||
<button class="btn btn-sm btn-primary btn-outline pull-right" ng-click="activity.refresh()" ng-disabled="activity.busy"><i class="fa fa-sync"></i></button>
|
||||
|
||||
<input class="form-control pull-right" style="width: 200px;" placeholder="Search" type="text" ng-model="activity.search" ng-model-options="{ debounce: 1000 }" ng-change="activity.updateFilter()" />
|
||||
</h3>
|
||||
<div class="text-left" ng-show="user.isAtLeastOwner">
|
||||
<h3>{{ 'emails.mailboxSharing.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<div class="row ng-hide" ng-hide="ready">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
<div class="card" ng-show="user.isAtLeastOwner" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>{{ 'emails.mailboxSharing.description' | tr }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="ready">
|
||||
<div class="col-xs-12">
|
||||
<table class="table table-hover" style="margin: 0;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 5%"><!-- Icon --></th>
|
||||
<th style="width: 20%">Time</th>
|
||||
<th style="width: 75%">Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody ng-show="activity.busy">
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">
|
||||
<i class="fa fa-circle-notch fa-spin"></i>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody ng-hide="activity.eventLogs.length || activity.busy">
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">
|
||||
<br>
|
||||
<br>
|
||||
Event Log is empty.
|
||||
<br>
|
||||
<br>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody ng-repeat="eventlog in activity.eventLogs" ng-hide="activity.busy">
|
||||
<tr ng-click="activity.showEventLogDetails(eventlog)" class="hand">
|
||||
<td class="no-wrap">
|
||||
<i class="fas fa-arrow-circle-left" ng-show="eventlog.type === 'delivered'" uib-tooltip="Outgoing"></i>
|
||||
<i class="fas fa-history" ng-show="eventlog.type === 'deferred'" uib-tooltip="Deferred"></i>
|
||||
<i class="fas fa-arrow-circle-right" ng-show="eventlog.type === 'received'" uib-tooltip="Incoming"></i>
|
||||
<i class="fas fa-align-justify" ng-show="eventlog.type === 'queued'" uib-tooltip="Queued"></i>
|
||||
<!-- <i class="fas fa-ban" ng-show="eventlog.details.spamStatus.indexOf('Yes,') === 0" uib-tooltip="Spam"></i> -->
|
||||
<i class="fas fa-minus-circle" ng-show="eventlog.type === 'denied'" uib-tooltip="Denied"></i>
|
||||
<i class="fas fa-hand-paper" ng-show="eventlog.type === 'bounce'" uib-tooltip="Bounce"></i>
|
||||
<i class="fas fa-filter" ng-show="eventlog.type === 'spam-learn'" uib-tooltip="Spam filter trained"></i>
|
||||
</td>
|
||||
<td class="no-wrap"><span uib-tooltip="{{ eventlog.ts | prettyLongDate }}" class="arrow">{{ eventlog.ts | prettyDate }}</span></td>
|
||||
<td class="elide-table-cell">
|
||||
<span ng-show="eventlog.type === 'delivered' || eventlog.type === 'queued' || eventlog.type === 'received' || eventlog.type === 'bounce' || eventlog.type === 'deferred'">{{ eventlog.mailFrom | prettyEmailAddresses }} <i class="fas fa-long-arrow-alt-right"></i> {{ eventlog.rcptTo | prettyEmailAddresses }}</span>
|
||||
<span ng-show="eventlog.type === 'denied'">Incoming connection from {{ eventlog.remote.ip }} denied</span>
|
||||
<span ng-show="eventlog.type === 'spam-learn'">Spam filter trained using mailbox content</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="activity.activeEventLog === eventlog">
|
||||
<td colspan="6">
|
||||
<pre class="eventlog-details">{{ eventlog | json }}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="row">
|
||||
<div class="col-md-2" style="padding-top: 12px;">
|
||||
<i class="fa fa-circle" ng-class="{ 'status-active': mailboxSharing.enabled, 'status-inactive': !mailboxSharing.enabled }"></i> {{ mailboxSharing.enabled ? 'main.statusEnabled' : 'main.statusDisabled' | tr }}
|
||||
</div>
|
||||
<div class="col-md-10 text-right">
|
||||
<button class="btn" ng-class="{ 'btn-danger': mailboxSharing.enabled, 'btn-primary': !mailboxSharing.enabled }" ng-click="mailboxSharing.submit()" ng-disabled="mailboxSharing.enable === mailboxSharing.enabled"><i class="fa fa-circle-notch fa-spin" ng-show="mailboxSharing.busy"></i> {{ mailboxSharing.enabled ? ('main.disableAction' | tr) : ('main.enableAction' | tr) }} </button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<div class="text-left" ng-show="user.isAtLeastOwner">
|
||||
<h3>{{ 'emails.settings.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<a class="btn btn-primary pull-right" href="/logs.html?id=mail" target="_blank">Show Raw Logs</a>
|
||||
<div class="card" ng-show="user.isAtLeastOwner" style="margin-bottom: 15px;">
|
||||
<p ng-bind-html=" 'emails.settings.info' | tr "></p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'emails.settings.location' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ mailLocation.currentLocation.subdomain + (!mailLocation.currentLocation.subdomain ? '' : '.') + mailLocation.currentLocation.domain.domain }}
|
||||
<a ng-hide="mailLocation.busy" href="" ng-click="mailLocation.show()"><i class="fa fa-edit text-small"></i></a> <!-- ng-disabled does not work for links -->
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'emails.settings.maxMailSize' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ maxEmailSize.currentSize | prettyDiskSize }} <a href="" ng-click="maxEmailSize.show()"><i class="fa fa-edit text-small"></i></a></span>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'emails.settings.acl' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ 'emails.settings.aclOverview' | tr:{ dnsblZonesCount: acl.dnsblZonesCount } }} <a href="" ng-click="acl.show()"><i class="fa fa-edit text-small"></i></a></span>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'emails.settings.solrFts' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right" ng-hide="solrConfig.currentConfig">
|
||||
<i class="fa fa-circle-notch fa-spin"></i>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right" ng-show="solrConfig.currentConfig">
|
||||
<span ng-show="solrConfig.currentConfig.enabled">
|
||||
{{ 'emails.settings.solrEnabled' | tr }}
|
||||
<span ng-show="solrConfig.running">/ {{ 'emails.settings.solrRunning' | tr }}</span>
|
||||
<span ng-hide="solrConfig.running">/ {{ 'emails.settings.solrNotRunning' | tr }}</span>
|
||||
</span>
|
||||
<span ng-hide="solrConfig.currentConfig.enabled">{{ 'emails.settings.solrDisabled' | tr }}</span>
|
||||
<a href="" ng-click="solrConfig.show()"><i class="fa fa-edit text-small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="mailLocation.busy">
|
||||
<div class="col-md-12" style="margin-top: 10px;">
|
||||
{{ 'emails.settings.changeDomainProgress' | tr }}
|
||||
<div style="display: flex; margin: 4px 0;">
|
||||
<div class="progress progress-striped active animateMe" style="flex-grow: 1;">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ mailLocation.percent }}%"></div>
|
||||
</div>
|
||||
<div ng-show="mailLocation.taskMinutesActive >= 2" class="text-danger hand" style="margin: 0 4px;" ng-click="mailLocation.stopTask()" uib-tooltip="Cancel Task"><i class="fas fa-times"></i></div>
|
||||
</div>
|
||||
<p>{{ mailLocation.message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+351
-34
@@ -1,58 +1,368 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular:false */
|
||||
/* global $:false */
|
||||
/* global $, angular, TASK_TYPES */
|
||||
|
||||
angular.module('Application').controller('EmailsController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
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('/'); });
|
||||
|
||||
$scope.ready = false;
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.domains = [];
|
||||
|
||||
$scope.activity = {
|
||||
busy: true,
|
||||
eventLogs: [],
|
||||
activeEventLog: null,
|
||||
currentPage: 1,
|
||||
perPage: 20,
|
||||
search: '',
|
||||
// this is required because we need to rewrite the MAIL_SERVER_NAME env var
|
||||
$scope.reconfigureEmailApps = function () {
|
||||
var installedApps = Client.getInstalledApps();
|
||||
for (var i = 0; i < installedApps.length; i++) {
|
||||
if (!installedApps[i].manifest.addons.email) continue;
|
||||
|
||||
refresh: function () {
|
||||
$scope.activity.busy = true;
|
||||
Client.repairApp(installedApps[i].id, { }, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Client.getMailEventLogs($scope.activity.search, $scope.activity.currentPage, $scope.activity.perPage, function (error, result) {
|
||||
if (error) return console.error('Failed to fetch mail eventlogs.', error);
|
||||
$scope.mailLocation = {
|
||||
busy: false,
|
||||
error: null,
|
||||
currentLocation: { domain: null, subdomain: '' },
|
||||
domain: null,
|
||||
subdomain: '',
|
||||
taskId: null,
|
||||
percent: 0,
|
||||
taskMinutesActive: 0,
|
||||
message: '',
|
||||
errorMessage: '',
|
||||
reconfigure: false,
|
||||
|
||||
$scope.activity.busy = false;
|
||||
stopTask: function () {
|
||||
Client.getLatestTaskByType(TASK_TYPES.TASK_CHANGE_MAIL_LOCATION, function (error, task) {
|
||||
if (error) return console.error(error);
|
||||
if (!task.id) return;
|
||||
|
||||
$scope.activity.eventLogs = result;
|
||||
Client.stopTask(task.id, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
showNextPage: function () {
|
||||
$scope.activity.currentPage++;
|
||||
$scope.activity.refresh();
|
||||
refresh: function () {
|
||||
Client.getMailLocation(function (error, location) {
|
||||
if (error) return console.error('Failed to get max email location', error);
|
||||
|
||||
$scope.mailLocation.currentLocation.subdomain = location.subdomain;
|
||||
$scope.mailLocation.currentLocation.domain = $scope.domains.find(function (d) { return location.domain === d.domain; });
|
||||
|
||||
Client.getLatestTaskByType(TASK_TYPES.TASK_CHANGE_MAIL_LOCATION, function (error, task) {
|
||||
if (error) return console.error(error);
|
||||
if (!task) return;
|
||||
|
||||
$scope.mailLocation.taskId = task.id;
|
||||
$scope.mailLocation.reconfigure = task.active; // if task is active when this view reloaded, reconfigure email apps when task done
|
||||
$scope.mailLocation.updateStatus();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
showPrevPage: function () {
|
||||
if ($scope.activity.currentPage > 1) $scope.activity.currentPage--;
|
||||
else $scope.activity.currentPage = 1;
|
||||
$scope.activity.refresh();
|
||||
show: function () {
|
||||
$scope.mailLocation.busy = false;
|
||||
$scope.mailLocation.error = null;
|
||||
|
||||
$scope.mailLocation.domain = $scope.mailLocation.currentLocation.domain;
|
||||
$scope.mailLocation.subdomain = $scope.mailLocation.currentLocation.subdomain;
|
||||
|
||||
$scope.mailLocationForm.$setUntouched();
|
||||
$scope.mailLocationForm.$setPristine();
|
||||
|
||||
$('#mailLocationModal').modal('show');
|
||||
},
|
||||
|
||||
showEventLogDetails: function (eventLog) {
|
||||
if ($scope.activity.activeEventLog === eventLog) $scope.activity.activeEventLog = null;
|
||||
else $scope.activity.activeEventLog = eventLog;
|
||||
updateStatus: function () {
|
||||
Client.getTask($scope.mailLocation.taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.mailLocation.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
$scope.mailLocation.taskId = null;
|
||||
$scope.mailLocation.busy = false;
|
||||
$scope.mailLocation.message = '';
|
||||
$scope.mailLocation.percent = 0;
|
||||
$scope.taskMinutesActive = 0;
|
||||
$scope.mailLocation.errorMessage = data.success ? '' : data.error.message;
|
||||
|
||||
if ($scope.mailLocation.reconfigure) $scope.reconfigureEmailApps();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.mailLocation.busy = true;
|
||||
$scope.mailLocation.percent = data.percent;
|
||||
$scope.mailLocation.message = data.message;
|
||||
$scope.mailLocation.taskMinutesActive = moment().diff(moment(data.creationTime), 'minutes');
|
||||
|
||||
window.setTimeout($scope.mailLocation.updateStatus, 1000);
|
||||
});
|
||||
},
|
||||
|
||||
updateFilter: function () {
|
||||
$scope.activity.currentPage = 1;
|
||||
$scope.activity.refresh();
|
||||
submit: function () {
|
||||
$scope.mailLocation.busy = true;
|
||||
|
||||
Client.setMailLocation($scope.mailLocation.subdomain, $scope.mailLocation.domain.domain, function (error, result) {
|
||||
if (error) {
|
||||
$scope.mailLocation.busy = false;
|
||||
$scope.mailLocation.error = error;
|
||||
return;
|
||||
}
|
||||
|
||||
// update UI immediately
|
||||
$scope.mailLocation.currentLocation = { subdomain: $scope.mailLocation.subdomain, domain: $scope.mailLocation.domain };
|
||||
|
||||
$scope.mailLocation.taskId = result.taskId;
|
||||
$scope.mailLocation.reconfigure = true; // reconfigure email apps when task done
|
||||
$scope.mailLocation.updateStatus();
|
||||
|
||||
Client.refreshConfig(); // update config.mailFqdn
|
||||
|
||||
$('#mailLocationModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.maxEmailSize = {
|
||||
busy: false,
|
||||
error: null,
|
||||
size: 0,
|
||||
currentSize: 0,
|
||||
|
||||
refresh: function () {
|
||||
Client.getMaxEmailSize(function (error, size) {
|
||||
if (error) return console.error('Failed to get max email size', error);
|
||||
|
||||
$scope.maxEmailSize.currentSize = size;
|
||||
});
|
||||
},
|
||||
|
||||
show: function() {
|
||||
$scope.maxEmailSize.busy = false;
|
||||
$scope.maxEmailSize.error = null;
|
||||
$scope.maxEmailSize.size = $scope.maxEmailSize.currentSize;
|
||||
|
||||
$scope.maxEmailSizeChangeForm.$setUntouched();
|
||||
$scope.maxEmailSizeChangeForm.$setPristine();
|
||||
|
||||
$('#maxEmailSizeChangeModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.maxEmailSize.busy = true;
|
||||
|
||||
Client.setMaxEmailSize($scope.maxEmailSize.size, function (error) {
|
||||
$scope.maxEmailSize.busy = false;
|
||||
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.maxEmailSize.currentSize = $scope.maxEmailSize.size;
|
||||
|
||||
$('#maxEmailSizeChangeModal').modal('hide');
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
$scope.mailboxSharing = {
|
||||
busy: false,
|
||||
error: null,
|
||||
enabled: null, // null means we have not refreshed yet
|
||||
|
||||
refresh: function () {
|
||||
Client.getMailboxSharing(function (error, enabled) {
|
||||
if (error) return console.error('Failed to get mailbox sharing', error);
|
||||
|
||||
$scope.mailboxSharing.enabled = enabled;
|
||||
});
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.mailboxSharing.busy = true;
|
||||
|
||||
Client.setMailboxSharing(!$scope.mailboxSharing.enabled, function (error) {
|
||||
// give sometime for mail server to restart
|
||||
$timeout(function () {
|
||||
$scope.mailboxSharing.busy = false;
|
||||
if (error) return console.error(error);
|
||||
$scope.mailboxSharing.enabled = !$scope.mailboxSharing.enabled;
|
||||
}, 3000);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.solrConfig = {
|
||||
busy: false,
|
||||
error: {},
|
||||
currentConfig: null, // null means not loaded yet
|
||||
enabled: false,
|
||||
running: false,
|
||||
enoughMemory: false,
|
||||
|
||||
refresh: function () {
|
||||
Client.getService('mail', function (error, result) {
|
||||
if (error) return console.log('Error getting status of mail conatiner', error);
|
||||
|
||||
$scope.solrConfig.enoughMemory = result.config.memoryLimit > (1024*1024*1024*2);
|
||||
$scope.solrConfig.running = result.healthcheck && result.healthcheck.solr.status;
|
||||
|
||||
Client.getSolrConfig(function (error, config) {
|
||||
if (error) return console.error('Failed to get solr config', error);
|
||||
|
||||
$scope.solrConfig.currentConfig = config;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
show: function() {
|
||||
$scope.solrConfig.busy = false;
|
||||
$scope.solrConfig.error = null;
|
||||
$scope.solrConfig.enabled = $scope.solrConfig.currentConfig.enabled;
|
||||
|
||||
$('#solrConfigModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function (newState) {
|
||||
$scope.solrConfig.busy = true;
|
||||
|
||||
Client.setSolrConfig(newState, function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$timeout(function () {
|
||||
$scope.solrConfig.busy = false;
|
||||
// FIXME: these values are fake. but cannot get current status from mail server since it might be restarting
|
||||
$scope.solrConfig.currentConfig.enabled = newState;
|
||||
$scope.solrConfig.running = newState;
|
||||
|
||||
$timeout(function () { $scope.solrConfig.refresh(); }, 20000); // get real values after 20 seconds
|
||||
|
||||
$('#solrConfigModal').modal('hide');
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.spamConfig = {
|
||||
busy: false,
|
||||
error: {},
|
||||
acl: { whitelist: [], blacklist: [] },
|
||||
customConfig: '',
|
||||
|
||||
config: '',
|
||||
blacklist: '', // currently, we don't support whitelist because it requires user to understand a bit more of what he is doing
|
||||
|
||||
refresh: function () {
|
||||
Client.getSpamCustomConfig(function (error, config) {
|
||||
if (error) return console.error('Failed to get custom spam config', error);
|
||||
|
||||
$scope.spamConfig.customConfig = config;
|
||||
});
|
||||
|
||||
Client.getSpamAcl(function (error, acl) {
|
||||
if (error) return console.error('Failed to get spam acl', error);
|
||||
|
||||
$scope.spamConfig.acl = acl;
|
||||
});
|
||||
},
|
||||
|
||||
show: function() {
|
||||
$scope.spamConfig.busy = false;
|
||||
$scope.spamConfig.error = {};
|
||||
|
||||
$scope.spamConfig.blacklist = $scope.spamConfig.acl.blacklist.join('\n');
|
||||
$scope.spamConfig.config = $scope.spamConfig.customConfig;
|
||||
|
||||
$scope.spamConfigChangeForm.$setUntouched();
|
||||
$scope.spamConfigChangeForm.$setPristine();
|
||||
|
||||
$('#spamConfigChangeModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.spamConfig.busy = true;
|
||||
$scope.spamConfig.error = {};
|
||||
|
||||
var blacklist = $scope.spamConfig.blacklist.split('\n').filter(function (l) { return l !== ''; });
|
||||
|
||||
Client.setSpamAcl({ blacklist: blacklist, whitelist: [] }, function (error) {
|
||||
if (error) {
|
||||
$scope.spamConfig.busy = false;
|
||||
$scope.spamConfig.error.blacklist = error.message;
|
||||
$scope.spamConfigChangeForm.blacklist.$setPristine();
|
||||
return;
|
||||
}
|
||||
|
||||
Client.setSpamCustomConfig($scope.spamConfig.config, function (error) {
|
||||
if (error) {
|
||||
$scope.spamConfig.busy = false;
|
||||
$scope.spamConfig.error.config = error.message;
|
||||
$scope.spamConfigChangeForm.config.$setPristine();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.spamConfig.busy = false;
|
||||
|
||||
$scope.spamConfig.refresh();
|
||||
|
||||
$('#spamConfigChangeModal').modal('hide');
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
$scope.acl = {
|
||||
busy: false,
|
||||
error: {},
|
||||
|
||||
dnsblZones: '',
|
||||
dnsblZonesCount: 0,
|
||||
|
||||
refresh: function () {
|
||||
Client.getDnsblConfig(function (error, result) {
|
||||
if (error) return console.error('Failed to get email acl', error);
|
||||
|
||||
$scope.acl.dnsblZones = result.zones.join('\n');
|
||||
$scope.acl.dnsblZonesCount = result.zones.length;
|
||||
});
|
||||
},
|
||||
|
||||
show: function() {
|
||||
$scope.acl.busy = false;
|
||||
$scope.acl.error = {};
|
||||
|
||||
$scope.aclChangeForm.$setUntouched();
|
||||
$scope.aclChangeForm.$setPristine();
|
||||
|
||||
$('#aclChangeModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.acl.busy = true;
|
||||
$scope.acl.error = {};
|
||||
|
||||
var zones = $scope.acl.dnsblZones.split('\n').filter(function (l) { return l !== ''; });
|
||||
|
||||
Client.setDnsblConfig(zones, function (error) {
|
||||
if (error) {
|
||||
$scope.acl.busy = false;
|
||||
$scope.acl.error.dnsblZones = error.message;
|
||||
$scope.aclChangeForm.dnsblZones.$setPristine();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.acl.busy = false;
|
||||
|
||||
$scope.acl.refresh();
|
||||
|
||||
$('#aclChangeModal').modal('hide');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
$scope.testEmail = {
|
||||
busy: false,
|
||||
error: {},
|
||||
@@ -117,10 +427,10 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati
|
||||
domain.outbound = mailConfig.relay.provider !== 'noop';
|
||||
|
||||
// do this even if no outbound since people forget to remove mailboxes
|
||||
Client.getMailboxes(domain.domain, function (error, mailboxes) {
|
||||
Client.getMailboxCount(domain.domain, function (error, count) {
|
||||
if (error) return console.error('Failed to fetch mailboxes for domain', domain.domain, error);
|
||||
|
||||
domain.mailboxCount = mailboxes.length;
|
||||
domain.mailboxCount = count;
|
||||
|
||||
Client.getMailUsage(domain.domain, function (error, usage) {
|
||||
if (error) return console.error('Failed to fetch usage for domain', domain.domain, error);
|
||||
@@ -130,7 +440,6 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@@ -141,7 +450,15 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati
|
||||
$scope.domains = domains;
|
||||
$scope.ready = true;
|
||||
|
||||
$scope.activity.refresh();
|
||||
if ($scope.user.isAtLeastOwner) {
|
||||
$scope.mailLocation.refresh();
|
||||
$scope.maxEmailSize.refresh();
|
||||
$scope.mailboxSharing.refresh();
|
||||
$scope.spamConfig.refresh();
|
||||
$scope.solrConfig.refresh();
|
||||
$scope.acl.refresh();
|
||||
}
|
||||
|
||||
refreshDomainStatuses();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
|
||||
<div>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<h1>Event Log</h1>
|
||||
<h1>{{ 'eventlog.title' | tr }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="filter">
|
||||
<input type="text" class="form-control" style="min-width: 350px;" ng-model="search" ng-model-options="{ debounce: 1000 }" ng-change="updateFilter()" placeholder="Search"/>
|
||||
<multiselect ng-model="selectedActions" ms-header="All Events" options="a.name for a in actions" data-multiple="true" ng-change="updateFilter(true)" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<div class="eventlog-filter">
|
||||
<input type="text" class="form-control" style="min-width: 350px;" ng-model="search" ng-model-options="{ debounce: 1000 }" ng-change="updateFilter()" placeholder="{{ 'main.searchPlaceholder' | tr }}"/>
|
||||
<multiselect ng-model="selectedActions" ms-header="{{ 'eventlog.filterAllEvents' | tr }}" options="a.name for a in actions" data-multiple="true" ng-change="updateFilter(true)" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<select class="form-control" ng-model="pageItems" ng-options="a.name for a in pageItemCount" ng-change="updateFilter(true)"></select>
|
||||
<!-- <select class="form-control" ng-model="action" ng-options="a.name for a in actions" ng-change="updateFilter()">
|
||||
<option value="">-- All actions --</option>
|
||||
</select> -->
|
||||
</div>
|
||||
<div class="pagination pull-right">
|
||||
<button class="btn btn-default btn-outline" ng-click="showPrevPage()" ng-disabled="busy || currentPage <= 1"><i class="fa fa-angle-double-left"></i> prev</button>
|
||||
<button class="btn btn-default btn-outline" ng-click="showNextPage()" ng-disabled="busy || pageItems.value > eventLogs.length">next <i class="fa fa-angle-double-right"></i></button>
|
||||
<button class="btn btn-default btn-outline" ng-click="refresh()"><i class="fas fa-sync-alt" ng-class="{ 'fa-spin': busyRefresh }"></i></button>
|
||||
<button class="btn btn-default btn-outline" ng-click="showPrevPage()" ng-disabled="busy || currentPage <= 1"><i class="fa fa-angle-double-left"></i> {{ 'main.pagination.prev' | tr }}</button>
|
||||
<button class="btn btn-default btn-outline" ng-click="showNextPage()" ng-disabled="busy || pageItems.value > eventLogs.length">{{ 'main.pagination.next' | tr }} <i class="fa fa-angle-double-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -30,9 +31,9 @@
|
||||
<table ng-hide="busy" class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-md-2">Time</th>
|
||||
<th class="col-md-3">Source</th>
|
||||
<th class="col-md-7">Details</th>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody ng-repeat="eventLog in eventLogs">
|
||||
@@ -0,0 +1,147 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular */
|
||||
/* global $ */
|
||||
|
||||
angular.module('Application').controller('EventLogController', ['$scope', '$location', '$translate', 'Client', function ($scope, $location, $translate, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
$scope.busy = false;
|
||||
$scope.busyRefresh = false;
|
||||
$scope.eventLogs = [];
|
||||
$scope.activeEventLog = null;
|
||||
|
||||
// TODO sync this with the eventlog filter
|
||||
$scope.actions = [
|
||||
{ name: '-- All app events --', value: 'app.' },
|
||||
{ name: '-- All user events --', value: 'user.' },
|
||||
{ name: 'app.backup', value: 'app.backup' },
|
||||
{ name: 'app.backup.finish', value: 'app.backup.finish' },
|
||||
{ name: 'app.configure', value: 'app.configure' },
|
||||
{ name: 'app.install', value: 'app.install' },
|
||||
{ name: 'app.restore', value: 'app.restore' },
|
||||
{ name: 'app.uninstall', value: 'app.uninstall' },
|
||||
{ name: 'app.update', value: 'app.update' },
|
||||
{ name: 'app.update.finish', value: 'app.update.finish' },
|
||||
{ name: 'app.login', value: 'app.login' },
|
||||
{ name: 'app.oom', value: 'app.oom' },
|
||||
{ name: 'app.down', value: 'app.down' },
|
||||
{ name: 'app.up', value: 'app.up' },
|
||||
{ name: 'app.start', value: 'app.start' },
|
||||
{ name: 'app.stop', value: 'app.stop' },
|
||||
{ name: 'app.restart', value: 'app.restart' },
|
||||
{ name: 'Apptask Crash', value: 'app.task.crash' },
|
||||
{ name: 'backup.cleanup', value: 'backup.cleanup.start' },
|
||||
{ name: 'backup.cleanup.finish', value: 'backup.cleanup.finish' },
|
||||
{ name: 'backup.finish', value: 'backup.finish' },
|
||||
{ name: 'backup.start', value: 'backup.start' },
|
||||
{ name: 'certificate.new', value: 'certificate.new' },
|
||||
{ name: 'certificate.renew', value: 'certificate.renew' },
|
||||
{ name: 'cloudron.activate', value: 'cloudron.activate' },
|
||||
{ name: 'cloudron.provision', value: 'cloudron.provision' },
|
||||
{ name: 'cloudron.restore', value: 'cloudron.restore' },
|
||||
{ name: 'cloudron.start', value: 'cloudron.start' },
|
||||
{ name: 'cloudron.update', value: 'cloudron.update' },
|
||||
{ name: 'cloudron.update.finish', value: 'cloudron.update.finish' },
|
||||
{ name: 'dashboard.domain.update', value: 'dashboard.domain.update' },
|
||||
{ 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: 'mail.location', value: 'mail.location' },
|
||||
{ name: 'mail.enabled', value: 'mail.enabled' },
|
||||
{ name: 'mail.box.add', value: 'mail.box.add' },
|
||||
{ name: 'mail.box.update', value: 'mail.box.update' },
|
||||
{ name: 'mail.box.remove', value: 'mail.box.remove' },
|
||||
{ name: 'mail.list.add', value: 'mail.list.add' },
|
||||
{ name: 'mail.list.update', value: 'mail.list.update' },
|
||||
{ name: 'mail.list.remove', value: 'mail.list.remove' },
|
||||
{ name: 'service.configure', value: 'service.configure' },
|
||||
{ name: 'service.rebuild', value: 'service.rebuild' },
|
||||
{ name: 'service.restart', value: 'service.restart' },
|
||||
{ name: 'support.ticket', value: 'support.ticket' },
|
||||
{ name: 'support.ssh', value: 'support.ssh' },
|
||||
{ name: 'user.add', value: 'user.add' },
|
||||
{ name: 'user.login', value: 'user.login' },
|
||||
{ name: 'user.logout', value: 'user.logout' },
|
||||
{ name: 'user.remove', value: 'user.remove' },
|
||||
{ name: 'user.transfer', value: 'user.transfer' },
|
||||
{ name: 'user.update', value: 'user.update' },
|
||||
{ name: 'volume.add', value: 'volume.add' },
|
||||
{ name: 'volume.update', value: 'volume.update' },
|
||||
{ name: 'volume.remove', value: 'volume.update' },
|
||||
{ name: 'System Crash', value: 'system.crash' }
|
||||
];
|
||||
|
||||
$scope.pageItemCount = [
|
||||
{ name: $translate.instant('main.pagination.perPageSelector', { n: 20 }), value: 20 },
|
||||
{ name: $translate.instant('main.pagination.perPageSelector', { n: 50 }), value: 50 },
|
||||
{ name: $translate.instant('main.pagination.perPageSelector', { n: 100 }), value: 100 }
|
||||
];
|
||||
|
||||
$scope.currentPage = 1;
|
||||
$scope.pageItems = $scope.pageItemCount[0];
|
||||
$scope.action = '';
|
||||
$scope.selectedActions = [];
|
||||
$scope.search = '';
|
||||
|
||||
function fetchEventLogs(background, callback) {
|
||||
callback = callback || function (error) { if (error) console.error(error); };
|
||||
background = background || false;
|
||||
|
||||
if (!background) $scope.busy = true;
|
||||
|
||||
var actions = $scope.selectedActions.map(function (a) { return a.value; }).join(', ');
|
||||
|
||||
Client.getEventLogs(actions, $scope.search || null, $scope.currentPage, $scope.pageItems.value, function (error, result) {
|
||||
$scope.busy = false;
|
||||
|
||||
if (error) return callback(error);
|
||||
|
||||
$scope.eventLogs = [];
|
||||
result.forEach(function (e) {
|
||||
$scope.eventLogs.push({ raw: e, details: Client.eventLogDetails(e), source: Client.eventLogSource(e) });
|
||||
});
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
$scope.refresh = function () {
|
||||
$scope.busyRefresh = true;
|
||||
|
||||
fetchEventLogs(true, function () {
|
||||
$scope.busyRefresh = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showNextPage = function () {
|
||||
$scope.currentPage++;
|
||||
fetchEventLogs();
|
||||
};
|
||||
|
||||
$scope.showPrevPage = function () {
|
||||
if ($scope.currentPage > 1) $scope.currentPage--;
|
||||
else $scope.currentPage = 1;
|
||||
|
||||
fetchEventLogs();
|
||||
};
|
||||
|
||||
$scope.updateFilter = function (fresh) {
|
||||
if (fresh) $scope.currentPage = 1;
|
||||
fetchEventLogs();
|
||||
};
|
||||
|
||||
$scope.showEventLogDetails = function (eventLog) {
|
||||
if ($scope.activeEventLog === eventLog) $scope.activeEventLog = null;
|
||||
else $scope.activeEventLog = eventLog;
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
fetchEventLogs();
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -1,42 +0,0 @@
|
||||
<div class="content content-large">
|
||||
|
||||
<div class="row" ng-if="errorMessage">
|
||||
<br>
|
||||
<div class="alert alert-danger text-center">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h2>Memory</h2>
|
||||
</div>
|
||||
|
||||
<div class="card card-large text-center">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h3>Apps</h3>
|
||||
<div style="width: 200px; height: 200px; margin: auto;">
|
||||
<canvas id="memoryUsageAppsChart" style="width: 200px; height: 200px;"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h3>System</h3>
|
||||
<div style="width: 200px; height: 200px; margin: auto;">
|
||||
<canvas id="memoryUsageSystemChart" style="width: 200px; height: 200px;"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4 ng-show="activeApp === 'system'">System</h4>
|
||||
<h4 ng-show="activeApp !== 'system'">{{ activeApp.fqdn }}</h4>
|
||||
<br/>
|
||||
<canvas id="memoryAppChart" width="900" height="300"></canvas>
|
||||
<p>Memory consumption in MB.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,248 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global Chart:false */
|
||||
/* global asyncForEach:false */
|
||||
/* global angular:false */
|
||||
/* global $:false */
|
||||
|
||||
angular.module('Application').controller('GraphsController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.memoryUsageSystem = [];
|
||||
$scope.memoryUsageApps = [];
|
||||
$scope.activeApp = null;
|
||||
$scope.memory = null;
|
||||
|
||||
$scope.errorMessage = '';
|
||||
|
||||
$scope.installedApps = Client.getInstalledApps();
|
||||
|
||||
function bytesToMegaBytes(value) {
|
||||
return (value/1024/1024).toFixed(2);
|
||||
}
|
||||
|
||||
// http://stackoverflow.com/questions/1484506/random-color-generator-in-javascript
|
||||
function getRandomColor() {
|
||||
var letters = '0123456789ABCDEF'.split('');
|
||||
var color = '#';
|
||||
for (var i = 0; i < 6; i++ ) {
|
||||
color += letters[Math.floor(Math.random() * 16)];
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
var colorIndex = 0;
|
||||
var colors = [ '#2196F3', '#3995b1', '#f0ad4e', '#ff4c4c' ];
|
||||
function getNextColor() {
|
||||
if (colors[colorIndex+1]) return colors[colorIndex++];
|
||||
return getRandomColor();
|
||||
}
|
||||
|
||||
$scope.setError = function (context, error) {
|
||||
$scope.errorMessage = 'Error loading ' + context + ' stats : ' + error.message + '. Try restarting the graphite service.';
|
||||
};
|
||||
|
||||
$scope.setMemoryApp = function (app, color) {
|
||||
$scope.activeApp = app;
|
||||
|
||||
var timePeriod = 12 * 60; // in minutes
|
||||
var timeBucketSize = 60; // in minutes
|
||||
|
||||
var target;
|
||||
if (app === 'system') target = 'summarize(sum(collectd.localhost.memory.memory-used, collectd.localhost.swap.swap-used), "' + timeBucketSize + 'min", "avg")';
|
||||
else target = 'summarize(collectd.localhost.table-' + app.id + '-memory.gauge-rss, "' + timeBucketSize + 'min", "avg")';
|
||||
|
||||
Client.graphs([target], '-' + timePeriod + 'min', {}, function (error, result) {
|
||||
if (error) return $scope.setError('memory', error);
|
||||
|
||||
// translate the data from bytes to MB
|
||||
var datapoints = result[0].datapoints.map(function (d) { return parseInt((d[0] / 1024 / 1024).toFixed(2)); });
|
||||
var labels = datapoints.map(function (d, index) {
|
||||
var dateTime = new Date(Date.now() - ((timePeriod - (index * timeBucketSize)) * 60 *1000));
|
||||
return ('0' + dateTime.getHours()).slice(-2) + ':00';
|
||||
});
|
||||
|
||||
var data = {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Memory',
|
||||
backgroundColor: color || '#82C4F8',
|
||||
borderColor: color || '#2196F3',
|
||||
borderWidth: 2,
|
||||
pointBackgroundColor: color || 'rgba(151,187,205,1)',
|
||||
pointBorderColor: color || '#2196F3',
|
||||
pointHoverBackgroundColor: color || '#82C4F8',
|
||||
pointHoverBorderColor: color || '#82C4F8',
|
||||
data: datapoints
|
||||
}]
|
||||
};
|
||||
|
||||
var scaleMax = 0;
|
||||
if ($scope.activeApp === 'system') {
|
||||
scaleMax = $scope.memory.memory + $scope.memory.swap;
|
||||
} else {
|
||||
scaleMax = $scope.activeApp.memoryLimit || $scope.activeApp.manifest.memoryLimit || (256 * 1024 * 1024);
|
||||
}
|
||||
|
||||
var stepSize;
|
||||
if (scaleMax >= (8 * 1024 * 1024 * 1024)) stepSize = 1024;
|
||||
else if (scaleMax >= (4 * 1024 * 1024 * 1024)) stepSize = 512;
|
||||
else if (scaleMax >= (2 * 1024 * 1024 * 1024)) stepSize = 256;
|
||||
else stepSize = 128;
|
||||
|
||||
var options = {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
scales: {
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
min: 0,
|
||||
max: Math.round(scaleMax / (1024 * 1024)),
|
||||
stepSize: stepSize,
|
||||
beginAtZero: true
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
var ctx = $('#memoryAppChart').get(0).getContext('2d');
|
||||
new Chart(ctx, { type: 'line', data: data, options: options });
|
||||
});
|
||||
};
|
||||
|
||||
$scope.updateMemorySystemChart = function () {
|
||||
var targets = [];
|
||||
var targetsInfo = [];
|
||||
|
||||
targets.push('summarize(collectd.localhost.memory.memory-used, "1min", "avg")');
|
||||
targetsInfo.push({ label: 'Memory (RAM)', color: '#2196F3' });
|
||||
|
||||
targets.push('summarize(collectd.localhost.swap.swap-used, "1min", "avg")');
|
||||
targetsInfo.push({ label: 'Swap', color: '#2196A9' });
|
||||
|
||||
targets.push('summarize(sum(collectd.localhost.memory.memory-buffered, collectd.localhost.memory.memory-cached), "1min", "avg")');
|
||||
targetsInfo.push({ label: 'Memory (Cached)', color: '#f0ad4e' });
|
||||
|
||||
targets.push('summarize(collectd.localhost.swap.swap-cached, "1min", "avg")');
|
||||
targetsInfo.push({ label: 'Swap (Cached)', color: '#f0cd4e' });
|
||||
|
||||
targets.push('summarize(collectd.localhost.memory.memory-free, "1min", "avg")');
|
||||
targetsInfo.push({ label: 'Memory (Free)', color: '#27DD65' });
|
||||
|
||||
targets.push('summarize(collectd.localhost.swap.swap-free, "1min", "avg")');
|
||||
targetsInfo.push({ label: 'Swap (Free)', color: '#27CE65' });
|
||||
|
||||
Client.graphs(targets, '-1min', {}, function (error, result) {
|
||||
if (error) return $scope.setError('memory', error);
|
||||
|
||||
$scope.memoryUsageSystem = result.map(function (data, index) {
|
||||
return {
|
||||
value: bytesToMegaBytes(data.datapoints[0][0]),
|
||||
color: targetsInfo[index].color,
|
||||
highlight: targetsInfo[index].color,
|
||||
label: targetsInfo[index].label
|
||||
};
|
||||
});
|
||||
|
||||
var tmp = {
|
||||
datasets: [{
|
||||
data: result.map(function (data) { return bytesToMegaBytes(data.datapoints[0][0]); }),
|
||||
backgroundColor: result.map(function (data, index) { return targetsInfo[index].color; })
|
||||
}],
|
||||
labels: result.map(function (data, index) { return targetsInfo[index].label; })
|
||||
};
|
||||
|
||||
var options = {
|
||||
onClick: function (/*event, dataset*/) {
|
||||
$scope.setMemoryApp('system');
|
||||
},
|
||||
legend: { display: false }
|
||||
};
|
||||
|
||||
var ctx = $('#memoryUsageSystemChart').get(0).getContext('2d');
|
||||
new Chart(ctx, { type: 'doughnut', data: tmp, options: options });
|
||||
});
|
||||
};
|
||||
|
||||
$scope.updateMemoryAppsChart = function () {
|
||||
var targets = [];
|
||||
var targetsInfo = [];
|
||||
|
||||
colorIndex = 0;
|
||||
$scope.installedApps.forEach(function (app) {
|
||||
targets.push('summarize(collectd.localhost.table-' + app.id + '-memory.gauge-rss, "1min", "avg")');
|
||||
targetsInfo.push({
|
||||
label: app.fqdn,
|
||||
color: getNextColor(),
|
||||
app: app
|
||||
});
|
||||
});
|
||||
|
||||
// we split up the request, to avoid too large query strings into graphite
|
||||
var tmp = [];
|
||||
var aggregatedResult= [];
|
||||
|
||||
while (targets.length > 0) tmp.push(targets.splice(0, 10));
|
||||
|
||||
asyncForEach(tmp, function (targets, callback) {
|
||||
Client.graphs(targets, '-1min', {}, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
aggregatedResult = aggregatedResult.concat(result);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) return $scope.setError('memory', error);
|
||||
|
||||
$scope.memoryUsageApps = aggregatedResult.map(function (data, index) {
|
||||
return {
|
||||
value: bytesToMegaBytes(data.datapoints[0][0]),
|
||||
color: targetsInfo[index].color,
|
||||
highlight: targetsInfo[index].color,
|
||||
label: targetsInfo[index].label
|
||||
};
|
||||
});
|
||||
|
||||
var tmp = {
|
||||
datasets: [{
|
||||
data: aggregatedResult.map(function (data) { return bytesToMegaBytes(data.datapoints[0][0]); }),
|
||||
backgroundColor: aggregatedResult.map(function (data, index) { return targetsInfo[index].color; })
|
||||
}],
|
||||
labels: aggregatedResult.map(function (data, index) { return targetsInfo[index].label; })
|
||||
};
|
||||
|
||||
var options = {
|
||||
onClick: function (event, dataset) {
|
||||
var selectedDataInfo = targetsInfo.find(function (info) { return info.label === dataset[0]._model.label; });
|
||||
if (selectedDataInfo) $scope.setMemoryApp(selectedDataInfo.app, selectedDataInfo.color);
|
||||
},
|
||||
legend: { display: false }
|
||||
};
|
||||
|
||||
var ctx = $('#memoryUsageAppsChart').get(0).getContext('2d');
|
||||
new Chart(ctx, { type: 'doughnut', data: tmp, options: options });
|
||||
});
|
||||
};
|
||||
|
||||
function getMemory(callback) {
|
||||
Client.memory(function (error, memory) {
|
||||
if (error) console.error(error);
|
||||
|
||||
$scope.memory = memory;
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
Client.onReady(function () {
|
||||
getMemory(function () {
|
||||
$scope.updateMemorySystemChart();
|
||||
$scope.updateMemoryAppsChart();
|
||||
$scope.setMemoryApp('system');
|
||||
});
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
+168
-36
@@ -3,31 +3,31 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Configure IP Provider</h4>
|
||||
<h4 class="modal-title">{{ 'network.configureIp.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="sysinfoForm" role="form" novalidate ng-submit="sysinfo.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Provider <sup><a ng-href="{{ config.webServerOrigin }}/documentation/networking/#ip-configuration" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<label class="control-label">{{ 'network.ip.provider' | tr }} <sup><a ng-href="https://docs.cloudron.io/networking/#ipv4" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="sysinfo.newProvider" ng-options="a.value as a.name for a in sysinfoProvider"></select>
|
||||
</div>
|
||||
|
||||
<div ng-show="sysinfo.newProvider === 'generic'">
|
||||
The Public IP address of the server will be <a href="https://api.cloudron.io/api/v1/helper/public_ip" target="_blank">automatically detected</a>.
|
||||
{{ 'network.configureIp.providerGenericDescription' | tr }} <sup><a ng-href="https://ipv4.api.cloudron.io/api/v1/helper/public_ip" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</div>
|
||||
|
||||
<!-- Fixed -->
|
||||
<div class="form-group" ng-show="sysinfo.newProvider === 'fixed'" ng-class="{ 'has-error': (!sysinfoForm.ip.$dirty && sysinfo.error.ip) }">
|
||||
<label class="control-label">IP Address</label>
|
||||
<input type="text" class="form-control" ng-model="sysinfo.newIp" name="ip" ng-disabled="sysinfo.busy" ng-required="sysinfo.newProvider === 'fixed'">
|
||||
<p class="has-error" ng-show="sysinfo.error.ip">{{ sysinfo.error.ip }}</p>
|
||||
<div class="form-group" ng-show="sysinfo.newProvider === 'fixed'" ng-class="{ 'has-error': (!sysinfoForm.ipv4.$dirty && sysinfo.error.ipv4) }">
|
||||
<label class="control-label">{{ 'network.ipv4.address' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="sysinfo.newIPv4" name="ipv4" ng-disabled="sysinfo.busy" ng-required="sysinfo.newProvider === 'fixed'">
|
||||
<p class="has-error" ng-show="sysinfo.error.ipv4">{{ sysinfo.error.ipv4 }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Network Interface -->
|
||||
<div class="form-group" ng-show="sysinfo.newProvider === 'network-interface'" ng-class="{ 'has-error': (!sysinfoForm.ifname.$dirty && sysinfo.error.ifname) }">
|
||||
<label class="control-label">Interface Name</label>
|
||||
<p>List available devices on the server with <code>ip -f inet -br addr</code></p>
|
||||
<label class="control-label">{{ 'network.ip.interface' | tr }}</label>
|
||||
<p>{{ 'network.ip.interfaceDescription' | tr }} <code>ip -f inet -br addr</code></p>
|
||||
<input type="text" class="form-control" ng-model="sysinfo.newIfname" name="ifname" ng-disabled="sysinfo.busy" ng-required="sysinfo.newProvider === 'network-interface'">
|
||||
<p class="has-error" ng-show="sysinfo.error.ifname">{{ sysinfo.error.ifname }}</p>
|
||||
</div>
|
||||
@@ -37,8 +37,80 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="sysinfo.submit()" ng-disabled="sysinfoForm.$invalid || sysinfo.busy"><i class="fa fa-circle-notch fa-spin" ng-show="sysinfo.busy"></i> Save</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="sysinfo.submit()" ng-disabled="sysinfoForm.$invalid || sysinfo.busy"><i class="fa fa-circle-notch fa-spin" ng-show="sysinfo.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal block list -->
|
||||
<div class="modal fade" id="blocklistModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'network.firewall.configure.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="blocklistChangeForm" role="form" novalidate ng-submit="blocklist.submit()" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'network.firewall.blockedIpRanges' | tr }}</label>
|
||||
<p class="small">{{ 'network.firewall.configure.description' | tr }}</p>
|
||||
<div class="has-error" ng-show="blocklist.error.blocklist">{{ blocklist.error.blocklist }}</div>
|
||||
<textarea ng-model="blocklist.blocklist" placeholder="{{ 'network.firewall.configure.blocklistPlaceholder' | tr }}" name="blocklist" class="form-control" ng-class="{ 'has-error': !blocklistChangeForm.blocklist.$dirty && blocklist.error.blocklist }" rows="4"></textarea>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit"/>
|
||||
</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="blocklist.submit()"><i class="fa fa-circle-notch fa-spin" ng-show="blocklist.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal IPv6 -->
|
||||
<div class="modal fade" id="ipv6ConfigureModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'network.configureIpv6.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="ipv6ConfigureForm" role="form" novalidate ng-submit="ipv6Configure.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'network.ip.provider' | tr }} <sup><a ng-href="https://docs.cloudron.io/networking/#ipv6" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="ipv6Configure.newProvider" ng-options="a.value as a.name for a in ipv6ConfigureProvider"></select>
|
||||
</div>
|
||||
|
||||
<div ng-show="ipv6Configure.newProvider === 'generic'">
|
||||
{{ 'network.configureIp.providerGenericDescription' | tr }} <sup><a ng-href="https://ipv6.api.cloudron.io/api/v1/helper/public_ip" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</div>
|
||||
|
||||
<!-- Fixed -->
|
||||
<div class="form-group" ng-show="ipv6Configure.newProvider === 'fixed'" ng-class="{ 'has-error': (!ipv6ConfigureForm.ipv4.$dirty && ipv6Configure.error.ipv6) }">
|
||||
<label class="control-label">{{ 'network.ipv6.address' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="ipv6Configure.newIPv6" name="ipv6" ng-disabled="ipv6Configure.busy" ng-required="ipv6Configure.newProvider === 'fixed'">
|
||||
<p class="has-error" ng-show="ipv6Configure.error.ipv6">{{ ipv6Configure.error.ipv6 }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Network Interface -->
|
||||
<div class="form-group" ng-show="ipv6Configure.newProvider === 'network-interface'" ng-class="{ 'has-error': (!ipv6ConfigureForm.ifname.$dirty && ipv6Configure.error.ifname) }">
|
||||
<label class="control-label">{{ 'network.ip.interface' | tr }}</label>
|
||||
<p>{{ 'network.ip.interfaceDescription' | tr }} <code>ip -f inet6 -br addr</code></p>
|
||||
<input type="text" class="form-control" ng-model="ipv6Configure.newIfname" name="ifname" ng-disabled="ipv6Configure.busy" ng-required="ipv6Configure.newProvider === 'network-interface'">
|
||||
<p class="has-error" ng-show="ipv6Configure.error.ifname">{{ ipv6Configure.error.ifname }}</p>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="ipv6ConfigureForm.$invalid || ipv6Configure.busy"/>
|
||||
</fieldset>
|
||||
</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="ipv6Configure.submit()" ng-disabled="ipv6ConfigureForm.$invalid || ipv6Configure.busy"><i class="fa fa-circle-notch fa-spin" ng-show="ipv6Configure.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,43 +118,44 @@
|
||||
|
||||
<div class="content">
|
||||
<div class="text-left">
|
||||
<h1>Network</h1>
|
||||
<h1>{{ 'network.title' | tr }}</h1>
|
||||
</div>
|
||||
|
||||
<!-- IPv4 -->
|
||||
<div class="text-left">
|
||||
<h3>IP Address</h3>
|
||||
<h3>{{ 'network.ip.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
Cloudron uses this IP address when setting up DNS records.
|
||||
{{ 'network.ip.description' | tr }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">Provider</span>
|
||||
<span class="text-muted">{{ 'network.ip.provider' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ prettySysinfoProviderName(sysinfo.provider) }}</span>
|
||||
<span>{{ prettyIpProviderName(sysinfo.provider) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">IP Address</span>
|
||||
<span class="text-muted">{{ 'network.ip.address' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span ng-show="sysinfo.ip">{{ sysinfo.ip }}</span>
|
||||
<span ng-show="!sysinfo.ip">{{ sysinfo.serverIp }} (detected)</span>
|
||||
<span ng-show="sysinfo.ipv4">{{ sysinfo.ipv4 }}</span>
|
||||
<span ng-show="!sysinfo.ipv4">{{ sysinfo.serverIPv4 }} ({{ 'network.ip.detected' | tr }})</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="sysinfo.ifname">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">Network Interface Name</span>
|
||||
<span class="text-muted">{{ 'network.ip.interface' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ sysinfo.ifname }}</span>
|
||||
@@ -93,38 +166,97 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-6 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="sysinfo.show()">Configure</button>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="sysinfo.show()">{{ 'network.ip.configure' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Firewall -->
|
||||
<div class="text-left" ng-show="user.isAtLeastOwner">
|
||||
<h3>{{ 'network.firewall.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" ng-show="user.isAtLeastOwner">
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'network.firewall.blockedIpRanges' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ 'network.firewall.blocklist' | tr:{ blockCount: blocklist.currentBlocklistLength } }} <a href="" ng-click="blocklist.show()"><i class="fa fa-edit text-small"></i></a></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IPv6 -->
|
||||
<div class="text-left">
|
||||
<h3>{{ 'network.ipv6.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{{ 'network.ipv6.description' | tr }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-xs-2">
|
||||
<span class="text-muted">{{ 'network.ip.provider' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-10 text-right">
|
||||
<span>{{ prettyIpProviderName(ipv6Configure.provider) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="ipv6Configure.provider !== 'noop'">
|
||||
<div class="col-xs-2">
|
||||
<span class="text-muted">{{ 'network.ip.address' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-10 text-right">
|
||||
<span ng-show="ipv6Configure.ipv6">{{ ipv6Configure.ipv6 }}</span>
|
||||
<span ng-show="!ipv6Configure.ipv6 && ipv6Configure.serverIPv6">{{ ipv6Configure.serverIPv6 }} ({{ 'network.ip.detected' | tr }})</span>
|
||||
<span ng-show="ipv6Configure.displayError" class="text-danger">{{ ipv6Configure.displayError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="ipv6Configure.ifname">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'network.ip.interface' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ ipv6Configure.ifname }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-6 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="ipv6Configure.show()">{{ 'network.ip.configure' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>Dynamic DNS</h3>
|
||||
<h3>{{ 'network.dyndns.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>
|
||||
Enable this option to keep all your DNS records in sync with a changing IP address.
|
||||
This is useful when Cloudron runs in a network with a frequently changing public IP address like a home connection.
|
||||
</p>
|
||||
<p>{{ 'network.dyndns.description' | tr }}</p>
|
||||
<p class="text-danger" ng-show="dyndnsConfigure.error"><br/>{{ dyndnsConfigure.error }}</p>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="dyndnsConfigure.enabled" name="dynamicDns" ng-disabled="dyndnsConfigure.busy"/> Use Dynamic DNS
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<span class="text-success text-bold" ng-show="dyndnsConfigure.success">Saved</span>
|
||||
<div class="col-md-2" style="padding-top: 12px;">
|
||||
<i class="fa fa-circle" ng-class="{ 'status-active': dyndnsConfigure.isEnabled, 'status-inactive': !dyndnsConfigure.isEnabled }"></i> {{ dyndnsConfigure.isEnabled ? 'main.statusEnabled' : 'main.statusDisabled' | tr }}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="dyndnsConfigure.submit()" ng-disabled="dyndnsConfigure.currentState === dyndnsConfigure.enabled"><i class="fa fa-circle-notch fa-spin" ng-show="dyndnsConfigure.busy"></i> Save</button>
|
||||
<div class="col-md-10 text-right">
|
||||
<button class="btn btn-outline btn-primary" ng-hide="dyndnsConfigure.isEnabled" ng-click="dyndnsConfigure.setEnabled(true)" ng-disabled="dyndnsConfigure.busy"><i class="fa fa-circle-notch fa-spin" ng-show="dyndnsConfigure.busy"></i> {{ 'main.enableAction' | tr }}</button>
|
||||
<button class="btn btn-outline btn-danger" ng-show="dyndnsConfigure.isEnabled" ng-click="dyndnsConfigure.setEnabled(false)" ng-disabled="dyndnsConfigure.busy"><i class="fa fa-circle-notch fa-spin" ng-show="dyndnsConfigure.busy"></i> {{ 'main.disableAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+159
-23
@@ -6,6 +6,7 @@
|
||||
angular.module('Application').controller('NetworkController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
// keep in sync with sysinfo.js
|
||||
@@ -15,8 +16,16 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
|
||||
{ name: 'Network Interface', value: 'network-interface' }
|
||||
];
|
||||
|
||||
$scope.prettySysinfoProviderName = function (provider) {
|
||||
$scope.ipv6ConfigureProvider = [
|
||||
{ name: 'Disabled', value: 'noop' },
|
||||
{ name: 'Public IP', value: 'generic' },
|
||||
{ name: 'Static IP Address', value: 'fixed' },
|
||||
{ name: 'Network Interface', value: 'network-interface' }
|
||||
];
|
||||
|
||||
$scope.prettyIpProviderName = function (provider) {
|
||||
switch (provider) {
|
||||
case 'noop': return 'Disabled';
|
||||
case 'generic': return 'Public IP';
|
||||
case 'fixed': return 'Static IP Address';
|
||||
case 'network-interface': return 'Network Interface';
|
||||
@@ -26,31 +35,156 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
|
||||
|
||||
$scope.dyndnsConfigure = {
|
||||
busy: false,
|
||||
success: false,
|
||||
error: '',
|
||||
currentState: false,
|
||||
enabled: false,
|
||||
isEnabled: false,
|
||||
|
||||
refresh: function () {
|
||||
Client.getDynamicDnsConfig(function (error, enabled) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.dyndnsConfigure.currentState = enabled;
|
||||
$scope.dyndnsConfigure.enabled = enabled;
|
||||
$scope.dyndnsConfigure.isEnabled = enabled;
|
||||
});
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
setEnabled: function (enabled) {
|
||||
$scope.dyndnsConfigure.busy = true;
|
||||
$scope.dyndnsConfigure.success = false;
|
||||
$scope.dyndnsConfigure.error = '';
|
||||
|
||||
Client.setDynamicDnsConfig($scope.dyndnsConfigure.enabled, function (error) {
|
||||
if (error) $scope.dyndnsConfigure.error = error.message;
|
||||
else $scope.dyndnsConfigure.currentState = $scope.dyndnsConfigure.enabled;
|
||||
|
||||
Client.setDynamicDnsConfig(enabled, function (error) {
|
||||
$scope.dyndnsConfigure.busy = false;
|
||||
$scope.dyndnsConfigure.success = true;
|
||||
|
||||
if (error) $scope.dyndnsConfigure.error = error.message;
|
||||
else $scope.dyndnsConfigure.isEnabled = enabled;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.ipv6Configure = {
|
||||
busy: false,
|
||||
error: {},
|
||||
displayError: null,
|
||||
|
||||
serverIPv6: '',
|
||||
|
||||
provider: '',
|
||||
ipv6: '',
|
||||
ifname: '',
|
||||
|
||||
// configure dialog
|
||||
newProvider: '',
|
||||
newIPv6: '',
|
||||
newIfname: '',
|
||||
|
||||
refresh: function () {
|
||||
Client.getIPv6Config(function (error, result) {
|
||||
if (error) {
|
||||
$scope.ipv6Configure.displayError = error.message;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
$scope.ipv6Configure.provider = result.provider;
|
||||
$scope.ipv6Configure.ipv6 = result.ipv6 || '';
|
||||
$scope.ipv6Configure.ifname = result.ifname || '';
|
||||
if (result.provider === 'noop') return;
|
||||
|
||||
Client.getServerIpv6(function (error, result) {
|
||||
if (error) {
|
||||
$scope.ipv6Configure.displayError = error.message;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
$scope.ipv6Configure.serverIPv6 = result.ipv6;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.ipv6Configure.error = {};
|
||||
$scope.ipv6Configure.newProvider = $scope.ipv6Configure.provider;
|
||||
$scope.ipv6Configure.newIPv6 = $scope.ipv6Configure.ipv6;
|
||||
$scope.ipv6Configure.newIfname = $scope.ipv6Configure.ifname;
|
||||
|
||||
$('#ipv6ConfigureModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.ipv6Configure.error = {};
|
||||
$scope.ipv6Configure.busy = true;
|
||||
|
||||
var config = {
|
||||
provider: $scope.ipv6Configure.newProvider
|
||||
};
|
||||
|
||||
if (config.provider === 'fixed') {
|
||||
config.ipv4 = $scope.ipv6Configure.newIPv4;
|
||||
} else if (config.provider === 'network-interface') {
|
||||
config.ifname = $scope.ipv6Configure.newIfname;
|
||||
}
|
||||
|
||||
Client.setIPv6Config(config, function (error) {
|
||||
$scope.ipv6Configure.busy = false;
|
||||
if (error && error.message.indexOf('ipv') !== -1) {
|
||||
$scope.ipv6Configure.error.ipv6 = error.message;
|
||||
$scope.ipv6ConfigureForm.$setPristine();
|
||||
$scope.ipv6ConfigureForm.$setUntouched();
|
||||
return;
|
||||
} else if (error && (error.message.indexOf('interface') !== -1 || error.message.indexOf('IPv6') !== -1)) {
|
||||
$scope.ipv6Configure.error.ifname = error.message;
|
||||
$scope.ipv6ConfigureForm.$setPristine();
|
||||
$scope.ipv6ConfigureForm.$setUntouched();
|
||||
return;
|
||||
} else if (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.ipv6Configure.refresh();
|
||||
|
||||
$('#ipv6ConfigureModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.blocklist = {
|
||||
busy: false,
|
||||
error: {},
|
||||
blocklist: '',
|
||||
currentBlocklist: '',
|
||||
currentBlocklistLength: 0,
|
||||
|
||||
refresh: function () {
|
||||
Client.getBlocklist(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.blocklist.currentBlocklist = result;
|
||||
$scope.blocklist.currentBlocklistLength = result.split('\n').filter(function (l) { return l.length !== 0 && l[0] !== '#'; }).length;
|
||||
});
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.blocklist.error = {};
|
||||
$scope.blocklist.blocklist = $scope.blocklist.currentBlocklist;
|
||||
|
||||
$('#blocklistModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.blocklist.error = {};
|
||||
$scope.blocklist.busy = true;
|
||||
|
||||
Client.setBlocklist($scope.blocklist.blocklist, function (error) {
|
||||
$scope.blocklist.busy = false;
|
||||
if (error) {
|
||||
$scope.blocklist.error.blocklist = error.message;
|
||||
$scope.blocklist.error.ip = error.message;
|
||||
$scope.blocklistChangeForm.$setPristine();
|
||||
$scope.blocklistChangeForm.$setUntouched();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.blocklist.refresh();
|
||||
|
||||
$('#blocklistModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -59,15 +193,15 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
|
||||
busy: false,
|
||||
error: {},
|
||||
|
||||
serverIp: '',
|
||||
serverIPv4: '',
|
||||
|
||||
provider: '',
|
||||
ip: '',
|
||||
ipv4: '',
|
||||
ifname: '',
|
||||
|
||||
// configure dialog
|
||||
newProvider: '',
|
||||
newIp: '',
|
||||
newIPv4: '',
|
||||
newIfname: '',
|
||||
|
||||
refresh: function () {
|
||||
@@ -75,13 +209,13 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.sysinfo.provider = result.provider;
|
||||
$scope.sysinfo.ip = result.ip || '';
|
||||
$scope.sysinfo.ipv4 = result.ipv4 || '';
|
||||
$scope.sysinfo.ifname = result.ifname || '';
|
||||
|
||||
Client.getServerIp(function (error, ip) {
|
||||
Client.getServerIpv4(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.sysinfo.serverIp = ip;
|
||||
$scope.sysinfo.serverIPv4 = result.ipv4;
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -89,7 +223,7 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
|
||||
show: function () {
|
||||
$scope.sysinfo.error = {};
|
||||
$scope.sysinfo.newProvider = $scope.sysinfo.provider;
|
||||
$scope.sysinfo.newIp = $scope.sysinfo.ip;
|
||||
$scope.sysinfo.newIPv4 = $scope.sysinfo.ipv4;
|
||||
$scope.sysinfo.newIfname = $scope.sysinfo.ifname;
|
||||
|
||||
$('#sysinfoModal').modal('show');
|
||||
@@ -104,15 +238,15 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
|
||||
};
|
||||
|
||||
if (config.provider === 'fixed') {
|
||||
config.ip = $scope.sysinfo.newIp;
|
||||
config.ipv4 = $scope.sysinfo.newIPv4;
|
||||
} else if (config.provider === 'network-interface') {
|
||||
config.ifname = $scope.sysinfo.newIfname;
|
||||
}
|
||||
|
||||
Client.setSysinfoConfig(config, function (error) {
|
||||
$scope.sysinfo.busy = false;
|
||||
if (error && error.message.indexOf('ip') !== -1) {
|
||||
$scope.sysinfo.error.ip = error.message;
|
||||
if (error && error.message.indexOf('ipv') !== -1) {
|
||||
$scope.sysinfo.error.ipv4 = error.message;
|
||||
$scope.sysinfoForm.$setPristine();
|
||||
$scope.sysinfoForm.$setUntouched();
|
||||
return;
|
||||
@@ -137,6 +271,8 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
|
||||
$scope.sysinfo.refresh();
|
||||
|
||||
$scope.dyndnsConfigure.refresh();
|
||||
$scope.ipv6Configure.refresh();
|
||||
if ($scope.user.isAtLeastOwner) $scope.blocklist.refresh();
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
|
||||
@@ -1,50 +1,37 @@
|
||||
<!-- Modal reboot server -->
|
||||
<div class="modal fade" id="rebootModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Really reboot server?</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-bold">Rebooting the server will cause temporary downtime for all apps installed on this Cloudron!</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="reboot.submit()" ng-disabled="reboot.busy"><i class="fa fa-circle-notch fa-spin" ng-show="reboot.busy"></i> Reboot now</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<div class="text-left">
|
||||
<h1>Notifications <button class="btn btn-primary btn-outline pull-right" ng-click="notifications.clearAll()" ng-disabled="clearAllBusy"><i class="fa fa-circle-notch fa-spin" ng-show="clearAllBusy"></i><i class="fa fa-check" ng-hide="clearAllBusy"></i> Clear All</button></h1>
|
||||
<h1>{{ 'notifications.title' | tr }}
|
||||
|
||||
<div class="title-toolbar">
|
||||
<button class="btn btn-default btn-outline" ng-click="showPrevPage()" ng-disabled="busy || currentPage <= 1"><i class="fa fa-angle-double-left"></i> {{ 'main.pagination.prev' | tr }}</button>
|
||||
<button class="btn btn-default btn-outline" ng-click="showNextPage()" ng-disabled="busy || perPage > notifications.length">{{ 'main.pagination.next' | tr }} <i class="fa fa-angle-double-right"></i></button>
|
||||
<button class="btn btn-primary btn-outline" ng-click="clearAll()" ng-disabled="!hasUnread || clearAllBusy"><i class="fa fa-circle-notch fa-spin" ng-show="clearAllBusy"></i><i class="fa fa-check" ng-hide="clearAllBusy"></i> {{ 'notifications.markAllAsRead' | tr }}</button>
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-12 text-center" ng-show="notifications.busy">
|
||||
<div class="col-lg-12 text-center" ng-show="busy">
|
||||
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
|
||||
<div class="card" ng-hide="notifications.busy || notifications.notifications.length">
|
||||
<div class="card" ng-hide="busy || notifications.length">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<h3 class="text-center" style="margin: 20px;">All Caught Up!</h3>
|
||||
<h3 class="text-center" style="margin: 20px;">{{ 'notifications.nonePending' | tr }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card notification-item" ng-repeat="notification in notifications.notifications">
|
||||
<div class="card notification-item" ng-repeat="notification in notifications" ng-class="{'notification-unread': !notification.acknowledged }" ng-click="notification.isCollapsed = !notification.isCollapsed">
|
||||
<div class="row">
|
||||
<div class="col-xs-12" ng-click="notification.isCollapsed = !notification.isCollapsed" ng-class="{ 'notification-details': notification.detailsShown }">
|
||||
{{ notification.title }} <small class="text-muted" uib-tooltip="{{ notification.creationTime | prettyLongDate }}">{{ notification.creationTime | prettyDate }}</small>
|
||||
<button class="btn btn-xs btn-default pull-right" ng-hide="notification.acknowledged" ng-click="notifications.ack(notification, $event)" uib-tooltip="Dismiss"><i class="fa fa-times"></i></button>
|
||||
|
||||
<div uib-collapse="notification.isCollapsed" expanding="notificationExpanding(notification)">
|
||||
<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>
|
||||
<div uib-collapse="notification.isCollapsed" expanding="ack(notification)">
|
||||
<br/>
|
||||
<p ng-hide="notification.messageJson" ng-click="$event.stopPropagation();" style="cursor: auto; overflow: auto;" ng-bind-html="notification.message | markdown2html"></p>
|
||||
<pre ng-show="notification.messageJson" ng-click="$event.stopPropagation();" style="cursor: auto">{{ notification.messageJson | json }}</pre>
|
||||
<button type="button" class="btn btn-danger pull-right" ng-click="$event.stopPropagation(); reboot.show(event)" ng-show="notification.title === 'Reboot Required'" ng-disabled="reboot.busy"><i class="fa fa-circle-notch fa-spin" ng-show="reboot.busy"></i> Reboot</button>
|
||||
<button type="button" class="btn btn-danger pull-right" ng-click="$event.stopPropagation(); $parent.reboot.show()" ng-show="notification.title === 'Reboot Required'" ng-disabled="reboot.busy"><i class="fa fa-circle-notch fa-spin" ng-show="reboot.busy"></i> {{ 'main.action.reboot' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+63
-85
@@ -1,115 +1,93 @@
|
||||
'use strict';
|
||||
|
||||
/* global asyncForEach, angular, $ */
|
||||
/* global async */
|
||||
/* global angular */
|
||||
|
||||
angular.module('Application').controller('NotificationsController', ['$scope', '$timeout', 'Client', function ($scope, $timeout, Client) {
|
||||
angular.module('Application').controller('NotificationsController', ['$scope', '$location', '$timeout', '$translate', '$interval', 'Client', function ($scope, $location, $timeout, $translate, $interval, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.clearAllBusy = false;
|
||||
|
||||
$scope.reboot = {
|
||||
busy: false,
|
||||
$scope.notifications = [];
|
||||
$scope.activeNotification = null;
|
||||
$scope.busy = true;
|
||||
$scope.hasUnread = false;
|
||||
$scope.currentPage = 1;
|
||||
$scope.perPage = 20;
|
||||
|
||||
show: function () {
|
||||
$scope.reboot.busy = false;
|
||||
$('#rebootModal').modal('show');
|
||||
},
|
||||
$scope.refresh = function () {
|
||||
Client.getNotifications({}, $scope.currentPage, $scope.perPage, function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
submit: function () {
|
||||
$scope.reboot.busy = true;
|
||||
// collapse by default
|
||||
result.forEach(function (r) { r.isCollapsed = true; });
|
||||
|
||||
Client.reboot(function (error) {
|
||||
if (error) return Client.error(error);
|
||||
|
||||
$('#rebootModal').modal('hide');
|
||||
|
||||
// trigger refetch to show offline banner
|
||||
$timeout(function () { Client.getStatus(function () {}); }, 5000);
|
||||
// attempt to translate or parse the message as json
|
||||
result.forEach(function (r) {
|
||||
try {
|
||||
r.messageJson = JSON.parse(r.message);
|
||||
} catch (e) {}
|
||||
});
|
||||
}
|
||||
|
||||
$scope.notifications = result;
|
||||
$scope.hasUnread = !!result.find(function (n) { return !n.acknowledged; });
|
||||
|
||||
$scope.busy = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.notifications = {
|
||||
notifications: [],
|
||||
activeNotification: null,
|
||||
busy: true,
|
||||
$scope.showNextPage = function () {
|
||||
$scope.currentPage++;
|
||||
$scope.refresh();
|
||||
};
|
||||
|
||||
refresh: function () {
|
||||
Client.getNotifications(false, 1, 100, function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
$scope.showPrevPage = function () {
|
||||
if ($scope.currentPage > 1) $scope.currentPage--;
|
||||
else $scope.currentPage = 1;
|
||||
|
||||
// collapse by default
|
||||
result.forEach(function (r) { r.isCollapsed = true; });
|
||||
$scope.refresh();
|
||||
};
|
||||
|
||||
// attempt to parse the message as json
|
||||
result.forEach(function (r) {
|
||||
try {
|
||||
r.messageJson = JSON.parse(r.message);
|
||||
} catch (e) {}
|
||||
});
|
||||
$scope.ack = function (notification) {
|
||||
if (notification.acknowledged) return;
|
||||
|
||||
$scope.notifications.notifications = result;
|
||||
Client.ackNotification(notification.id, true, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
$scope.notifications.busy = false;
|
||||
});
|
||||
},
|
||||
notification.acknowledged = true;
|
||||
$scope.$parent.notificationAcknowledged();
|
||||
});
|
||||
};
|
||||
|
||||
clicked: function (notification) {
|
||||
if ($scope.notifications.activeNotification === notification) return $scope.notifications.activeNotification = null;
|
||||
$scope.notifications.activeNotification = notification;
|
||||
},
|
||||
$scope.clearAll = function () {
|
||||
$scope.clearAllBusy = true;
|
||||
|
||||
ack: function (notification, event, callback) {
|
||||
callback = callback || function (error) { if (error) console.error(error); };
|
||||
async.eachLimit($scope.notifications, 20, function (notification, callback) {
|
||||
if (notification.acknowledged) return callback();
|
||||
|
||||
if (event) event.stopPropagation();
|
||||
|
||||
Client.ackNotification(notification.id, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
$scope.$parent.notificationAcknowledged(notification.id);
|
||||
$scope.notifications.refresh();
|
||||
Client.ackNotification(notification.id, true, function (error) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
} else {
|
||||
notification.acknowledged = true;
|
||||
$scope.$parent.notificationAcknowledged();
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
action: function (notification) {
|
||||
if (notification.action) window.location = notification.action;
|
||||
},
|
||||
|
||||
clearAll: function () {
|
||||
$scope.clearAllBusy = true;
|
||||
|
||||
asyncForEach($scope.notifications.notifications, function (notification, callback) {
|
||||
if (notification.acknowledged) return callback();
|
||||
$scope.notifications.ack(notification, null /* no click event */, callback);
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
$scope.clearAllBusy = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.notificationExpanding = function (notification) {
|
||||
if (!notification.eventId) return;
|
||||
|
||||
notification.busyLoadEvent = true;
|
||||
|
||||
Client.getEvent(notification.eventId, function (error, result) {
|
||||
notification.busyLoadEvent = false;
|
||||
|
||||
if (error) return console.error(error);
|
||||
|
||||
notification.event = result;
|
||||
$scope.hasUnread = false;
|
||||
$scope.clearAllBusy = false;
|
||||
});
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
$scope.notifications.refresh();
|
||||
var refreshTimer = $interval($scope.refresh, 60 * 1000); // keep this interval in sync with the notification count indicator in main.js
|
||||
$scope.$on('$destroy', function () {
|
||||
$interval.cancel(refreshTimer);
|
||||
});
|
||||
$scope.refresh();
|
||||
});
|
||||
|
||||
Client.onReconnect(function () {
|
||||
$scope.notifications.refresh();
|
||||
})
|
||||
}]);
|
||||
|
||||
+240
-208
@@ -4,163 +4,179 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Change your Avatar</h4>
|
||||
<h4 class="modal-title">{{ 'profile.changeAvatar.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body settings-avatar-selector">
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="useGravatar" ng-model="avatarChange.useGravatar" value="true_string">
|
||||
Use <a target="_blank" href="https://gravatar.com/">Gravatar</a>
|
||||
</label>
|
||||
<div style="margin: auto; text-align: left">
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="avatarType" ng-model="avatarChange.type" value="">
|
||||
{{ 'profile.changeAvatar.noAvatar' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="avatarType" ng-model="avatarChange.type" value="gravatar">
|
||||
<span ng-bind-html="'profile.changeAvatar.useGravatar' | tr:{ gravatarLink: 'https://gravatar.com/' }"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="avatarType" ng-model="avatarChange.type" value="custom">
|
||||
{{ 'profile.changeAvatar.useCustomPicture' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="useGravatar" ng-model="avatarChange.useGravatar" value="">
|
||||
Use Custom Picture
|
||||
</label>
|
||||
</div>
|
||||
<div ng-hide="avatarChange.useGravatar" class="preview-avatar">
|
||||
<div ng-show="avatarChange.type === 'custom'" class="preview-avatar">
|
||||
<img id="previewAvatar" width="128" height="128" class="copy" ng-click="avatarChange.showCustomAvatarSelector()"/>
|
||||
<input type="file" id="avatarFileInput" style="display: none" accept="image/png"/>
|
||||
<input type="file" id="avatarFileInput" style="display: none" accept="image/*"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="avatarChange.doChangeAvatar()" ng-disabled="avatarChange.busy || (avatarChange.useGravatarOrig === avatarChange.useGravatar && avatarChange.useGravatar) || (!avatarChange.useGravatar && !avatarChange.pictureChanged)"><i class="fa fa-circle-notch fa-spin" ng-show="avatarChange.busy"></i> Save</button>
|
||||
<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.doChangeAvatar()" ng-disabled="avatarChange.busy || (avatarChange.typeOrig === avatarChange.type && !avatarChange.pictureChanged) || (avatarChange.type === 'custom' && !avatarChange.pictureChanged)"><i class="fa fa-circle-notch fa-spin" ng-show="avatarChange.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Modal change password -->
|
||||
<div class="modal fade" id="passwordChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Change your password</h4>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'profile.changePassword.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="passwordChangeForm" role="form" novalidate ng-submit="passwordchange.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!passwordChangeForm.password.$dirty && passwordchange.error.password) || (passwordChangeForm.password.$dirty && passwordChangeForm.password.$invalid) }">
|
||||
<label class="control-label" for="inputPasswordChangePassword">{{ 'profile.changePassword.currentPassword' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!passwordChangeForm.password.$dirty && passwordchange.error.password) || (passwordChangeForm.password.$dirty && passwordChangeForm.password.$invalid)">
|
||||
<small ng-show="!passwordChangeForm.password.$dirty && passwordchange.error.password">Wrong password</small>
|
||||
<small ng-show="passwordChangeForm.password.$dirty && passwordChangeForm.password.$error.required">A password is required</small>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="passwordChangeForm" role="form" novalidate ng-submit="passwordchange.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!passwordChangeForm.password.$dirty && passwordchange.error.password) || (passwordChangeForm.password.$dirty && passwordChangeForm.password.$invalid) }">
|
||||
<label class="control-label" for="inputPasswordChangePassword">Current password</label>
|
||||
<div class="control-label" ng-show="(!passwordChangeForm.password.$dirty && passwordchange.error.password) || (passwordChangeForm.password.$dirty && passwordChangeForm.password.$invalid)">
|
||||
<small ng-show="!passwordChangeForm.password.$dirty && passwordchange.error.password">Wrong password</small>
|
||||
<small ng-show="passwordChangeForm.password.$dirty && passwordChangeForm.password.$error.required">A password is required</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="passwordchange.password" id="inputPasswordChangePassword" name="password" required autofocus>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!passwordChangeForm.newPassword.$dirty && passwordchange.error.newPassword) || (passwordChangeForm.newPassword.$dirty && passwordChangeForm.newPassword.$invalid) }">
|
||||
<label class="control-label" for="inputPasswordChangeNewPassword">New password</label>
|
||||
<div class="control-label" ng-show="(!passwordChangeForm.newPassword.$dirty && passwordchange.error.newPassword) || (passwordChangeForm.newPassword.$dirty && passwordChangeForm.newPassword.$invalid)">
|
||||
<small ng-show="!passwordChangeForm.newPassword.$dirty && passwordchange.error.newPassword">{{ passwordchange.error.newPassword }}<br/><br/></small>
|
||||
<small ng-show=" passwordChangeForm.newPassword.$dirty && passwordChangeForm.newPassword.$invalid">Password must be atleast 8 and at most 265 characters</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="passwordchange.newPassword" id="inputPasswordChangeNewPassword" name="newPassword" ng-minlength="8" ng-maxlength="256" required autofocus>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!passwordChangeForm.newPassword.$dirty && passwordchange.error.newPassword) || (passwordChangeForm.newPasswordRepeat.$dirty && passwordChangeForm.newPasswordRepeat.$error.required) || (passwordChangeForm.newPasswordRepeat.$dirty && passwordchange.newPassword !== passwordchange.newPasswordRepeat) }">
|
||||
<label class="control-label" for="inputPasswordChangeNewPasswordRepeat">Repeat new password</label>
|
||||
<div class="control-label" ng-show="(!passwordChangeForm.newPassword.$dirty && passwordchange.error.newPassword) || (passwordChangeForm.newPasswordRepeat.$dirty && passwordChangeForm.newPasswordRepeat.$error.required) || (passwordChangeForm.newPasswordRepeat.$dirty && passwordchange.newPassword !== passwordchange.newPasswordRepeat)">
|
||||
<small ng-show="passwordChangeForm.newPasswordRepeat.$dirty && passwordChangeForm.newPasswordRepeat.$error.required">A password is required</small>
|
||||
<small ng-show="passwordChangeForm.newPasswordRepeat.$dirty && passwordchange.newPassword !== passwordchange.newPasswordRepeat && passwordchange.newPasswordRepeat">Passwords don't match</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="passwordchange.newPasswordRepeat" id="inputPasswordChangeNewPasswordRepeat" name="newPasswordRepeat" required autofocus>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="passwordChangeForm.$invalid"/>
|
||||
</form>
|
||||
<input type="password" class="form-control" ng-model="passwordchange.password" id="inputPasswordChangePassword" name="password" required autofocus password-reveal>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!passwordChangeForm.newPassword.$dirty && passwordchange.error.newPassword) || (passwordChangeForm.newPassword.$dirty && passwordChangeForm.newPassword.$invalid) }">
|
||||
<label class="control-label" for="inputPasswordChangeNewPassword">{{ 'profile.changePassword.newPassword' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!passwordChangeForm.newPassword.$dirty && passwordchange.error.newPassword) || (passwordChangeForm.newPassword.$dirty && passwordChangeForm.newPassword.$invalid)">
|
||||
<small ng-show="!passwordChangeForm.newPassword.$dirty && passwordchange.error.newPassword">{{ passwordchange.error.newPassword }}<br/><br/></small>
|
||||
<small ng-show=" passwordChangeForm.newPassword.$dirty && passwordChangeForm.newPassword.$invalid">{{ 'profile.changePassword.errorPasswordInvalid' | tr }}</small>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="passwordchange.submit()" ng-disabled="passwordChangeForm.$invalid || passwordchange.busy"><i class="fa fa-circle-notch fa-spin" ng-show="passwordchange.busy"></i> Change</button>
|
||||
<input type="password" class="form-control" ng-model="passwordchange.newPassword" id="inputPasswordChangeNewPassword" name="newPassword" ng-minlength="8" ng-maxlength="256" required autofocus password-reveal>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!passwordChangeForm.newPassword.$dirty && passwordchange.error.newPassword) || (passwordChangeForm.newPasswordRepeat.$dirty && passwordChangeForm.newPasswordRepeat.$error.required) || (passwordChangeForm.newPasswordRepeat.$dirty && passwordchange.newPassword !== passwordchange.newPasswordRepeat) }">
|
||||
<label class="control-label" for="inputPasswordChangeNewPasswordRepeat">{{ 'profile.changePassword.newPasswordRepeat' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!passwordChangeForm.newPassword.$dirty && passwordchange.error.newPassword) || (passwordChangeForm.newPasswordRepeat.$dirty && passwordChangeForm.newPasswordRepeat.$error.required) || (passwordChangeForm.newPasswordRepeat.$dirty && passwordchange.newPassword !== passwordchange.newPasswordRepeat)">
|
||||
<small ng-show="passwordChangeForm.newPasswordRepeat.$dirty && passwordChangeForm.newPasswordRepeat.$error.required">{{ 'profile.changePassword.errorPasswordRequired' | tr }}</small>
|
||||
<small ng-show="passwordChangeForm.newPasswordRepeat.$dirty && passwordchange.newPassword !== passwordchange.newPasswordRepeat && passwordchange.newPasswordRepeat">{{ 'profile.changePassword.errorPasswordsDontMatch' | tr }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="passwordchange.newPasswordRepeat" id="inputPasswordChangeNewPasswordRepeat" name="newPasswordRepeat" required autofocus password-reveal>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="passwordChangeForm.$invalid"/>
|
||||
</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="passwordchange.submit()" ng-disabled="passwordChangeForm.$invalid || passwordchange.busy"><i class="fa fa-circle-notch fa-spin" ng-show="passwordchange.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change email -->
|
||||
<div class="modal fade" id="emailChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Change primary email address</h4>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<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)">
|
||||
<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>
|
||||
</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)}">
|
||||
<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">A valid email address is required</small>
|
||||
<small ng-show="(emailChangeForm.email.$dirty && emailChangeForm.email.$invalid) && !emailChangeForm.email.$error.required">The Email address is not valid</small>
|
||||
<small ng-show="!emailChangeForm.email.$dirty && emailchange.error.email">{{ emailchange.error.email }}</small>
|
||||
</div>
|
||||
<input type="email" class="form-control" ng-model="emailchange.email" id="inputEmailChangeEmail" name="email" required autofocus>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="emailChangeForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</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> Change</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="emailChangeForm.$invalid"/>
|
||||
</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="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>
|
||||
</div>
|
||||
|
||||
<!-- Modal change fallback email -->
|
||||
<div class="modal fade" id="fallbackEmailChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Change password recovery email address</h4>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'profile.changeFallbackEmail.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="fallbackEmailChangeForm" role="form" novalidate ng-submit="fallbackEmailChange.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': (fallbackEmailChangeForm.email.$dirty && fallbackEmailChangeForm.email.$invalid) || (!fallbackEmailChangeForm.email.$dirty && fallbackEmailChange.error.email)}">
|
||||
<label class="control-label" for="inputFallbackEmailChangeEmail">{{ 'profile.changeFallbackEmail.email' | tr }}</label>
|
||||
<input type="email" class="form-control" ng-model="fallbackEmailChange.email" id="inputFallbackEmailChangeEmail" name="email" required autofocus>
|
||||
<div class="control-label" ng-show="(!fallbackEmailChangeForm.email.$dirty && fallbackEmailChange.error.email) || (fallbackEmailChangeForm.email.$dirty && fallbackEmailChangeForm.email.$invalid)">
|
||||
<small ng-show="fallbackEmailChangeForm.email.$error.required">{{ 'profile.changeFallbackEmail.errorEmailRequired' | tr }}</small>
|
||||
<small ng-show="(fallbackEmailChangeForm.email.$dirty && fallbackEmailChangeForm.email.$invalid) && !fallbackEmailChangeForm.email.$error.required">{{ 'profile.changeFallbackEmail.errorEmailInvalid' | tr }}</small>
|
||||
<small ng-show="!fallbackEmailChangeForm.email.$dirty && fallbackEmailChange.error.email">{{ fallbackEmailChange.error.email }}</small>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="fallbackEmailChangeForm" role="form" novalidate ng-submit="fallbackEmailChange.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (fallbackEmailChangeForm.email.$dirty && fallbackEmailChangeForm.email.$invalid) || (!fallbackEmailChangeForm.email.$dirty && fallbackEmailChange.error.email)}">
|
||||
<div class="control-label" ng-show="(!fallbackEmailChangeForm.email.$dirty && fallbackEmailChange.error.email) || (fallbackEmailChangeForm.email.$dirty && fallbackEmailChangeForm.email.$invalid)">
|
||||
<small ng-show="fallbackEmailChangeForm.email.$error.required">A valid email address is required</small>
|
||||
<small ng-show="(fallbackEmailChangeForm.email.$dirty && fallbackEmailChangeForm.email.$invalid) && !fallbackEmailChangeForm.email.$error.required">The Email address is not valid</small>
|
||||
<small ng-show="!fallbackEmailChangeForm.email.$dirty && fallbackEmailChange.error.email">{{ fallbackEmailChange.error.email }}</small>
|
||||
</div>
|
||||
<input type="email" class="form-control" ng-model="fallbackEmailChange.email" id="inputfallbackEmailChangeEmail" name="email" required autofocus>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="fallbackEmailChangeForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!fallbackEmailChangeForm.password.$dirty && fallbackEmailChange.error.password) || (fallbackEmailChangeForm.password.$dirty && fallbackEmailChangeForm.password.$invalid) }">
|
||||
<label class="control-label" for="inputFallbackEmailChangePassword">{{ 'profile.changeFallbackEmail.password' | tr }}</label>
|
||||
<input type="password" class="form-control" ng-model="fallbackEmailChange.password" id="inputFallbackEmailChangePassword" name="password" required autofocus password-reveal>
|
||||
<div class="control-label" ng-show="(!fallbackEmailChangeForm.password.$dirty && fallbackEmailChange.error.password) || (fallbackEmailChangeForm.password.$dirty && fallbackEmailChangeForm.password.$invalid)">
|
||||
<small ng-show="!fallbackEmailChangeForm.password.$dirty && fallbackEmailChange.error.password">{{ 'profile.changeFallbackEmail.errorWrongPassword' | tr }}</small>
|
||||
<small ng-show="fallbackEmailChangeForm.password.$dirty && fallbackEmailChangeForm.password.$error.required">{{ 'profile.changeFallbackEmail.errorPasswordRequired' | tr }}</small>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="fallbackEmailChange.submit()" ng-disabled="fallbackEmailChangeForm.$invalid || fallbackEmailChange.busy"><i class="fa fa-circle-notch fa-spin" ng-show="fallbackEmailChange.busy"></i> Change</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="fallbackEmailChangeForm.$invalid"/>
|
||||
</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="fallbackEmailChange.submit()" ng-disabled="fallbackEmailChangeForm.$invalid || fallbackEmailChange.busy"><i class="fa fa-circle-notch fa-spin" ng-show="fallbackEmailChange.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change displayName -->
|
||||
<div class="modal fade" id="displayNameChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Change your display name</h4>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'profile.changeDisplayName.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="displayNameChangeForm" role="form" novalidate ng-submit="displayNameChange.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (displayNameChangeForm.displayName.$dirty && displayNameChangeForm.displayName.$invalid) || (!displayNameChangeForm.displayName.$dirty && displayNameChange.error.displayName)}">
|
||||
<input type="text" class="form-control" ng-model="displayNameChange.displayName" id="inputDisplayNameChangeDisplayName" name="displayName" required autofocus>
|
||||
<div class="control-label" ng-show="(!displayNameChangeForm.displayName.$dirty && displayNameChange.error.displayName) || (displayNameChangeForm.displayName.$dirty && displayNameChangeForm.displayName.$invalid)">
|
||||
<small ng-show="displayNameChangeForm.displayName.$error.required">{{ 'profile.changeDisplayName.errorDisplayNameRequired' | tr }}</small>
|
||||
<small ng-show="(displayNameChangeForm.displayName.$dirty && displayNameChangeForm.displayName.$invalid) && !displayNameChangeForm.displayName.$error.required">{{ 'profile.changeDisplayName.errorNameInvalid' | tr }}</small>
|
||||
<small ng-show="!displayNameChangeForm.email.$dirty && displayNameChange.error.displayName">{{ displayNameChange.error.displayName }}</small>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="displayNameChangeForm" role="form" novalidate ng-submit="displayNameChange.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (displayNameChangeForm.displayName.$dirty && displayNameChangeForm.displayName.$invalid) || (!displayNameChangeForm.displayName.$dirty && displayNameChange.error.displayName)}">
|
||||
<label class="control-label">Display name</label>
|
||||
<div class="control-label" ng-show="(!displayNameChangeForm.displayName.$dirty && displayNameChange.error.displayName) || (displayNameChangeForm.displayName.$dirty && displayNameChangeForm.displayName.$invalid)">
|
||||
<small ng-show="displayNameChangeForm.displayName.$error.required">A valid display name is required</small>
|
||||
<small ng-show="(displayNameChangeForm.displayName.$dirty && displayNameChangeForm.displayName.$invalid) && !displayNameChangeForm.displayName.$error.required">This display name is not valid</small>
|
||||
<small ng-show="!displayNameChangeForm.email.$dirty && displayNameChange.error.displayName">{{ displayNameChange.error.displayName }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="displayNameChange.displayName" id="inputDisplayNameChangeDisplayName" name="displayName" required autofocus>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="displayNameChangeForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="displayNameChange.submit()" ng-disabled="displayNameChangeForm.$invalid || displayNameChange.busy"><i class="fa fa-circle-notch fa-spin" ng-show="displayNameChange.busy"></i> Change</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="displayNameChangeForm.$invalid"/>
|
||||
</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="displayNameChange.submit()" ng-disabled="displayNameChangeForm.$invalid || displayNameChange.busy"><i class="fa fa-circle-notch fa-spin" ng-show="displayNameChange.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal enable twofactor authentication -->
|
||||
@@ -168,15 +184,14 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Enable Two-Factor Authentication</h4>
|
||||
<h4 class="modal-title">{{ 'profile.enable2FA.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body text-center" ng-hide="twoFactorAuthentication.secret">
|
||||
<p class="modal-body" ng-show="twoFactorAuthentication.mandatory2FAHelp && !twoFactorAuthentication.secret">{{ 'profile.enable2FA.description' | tr }}</p>
|
||||
<div class="modal-body text-center" ng-show="!twoFactorAuthentication.mandatory2FAHelp && !twoFactorAuthentication.secret">
|
||||
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
<div class="modal-body" ng-show="twoFactorAuthentication.secret">
|
||||
<p>
|
||||
Use Google Authenticator (<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" target="_blank">Android</a>, <a href="https://itunes.apple.com/us/app/google-authenticator/id388497605" target="_blank">iOS</a>), FreeOTP authenticator (<a href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp" target="_blank">Android</a>, <a href="https://itunes.apple.com/us/app/freeotp-authenticator/id872559395" target="_blank">iOS</a>) or a similar TOTP app to scan the secret.
|
||||
</p>
|
||||
<p ng-bind-html="'profile.enable2FA.authenticatorAppDescription' | tr:{ googleAuthenticatorPlayStoreLink: 'https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2', googleAuthenticatorITunesLink: 'https://itunes.apple.com/us/app/google-authenticator/id388497605', freeOTPPlayStoreLink: 'https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp', freeOTPITunesLink: 'https://itunes.apple.com/us/app/freeotp-authenticator/id872559395' }"></p>
|
||||
<center>
|
||||
<img ng-src="{{ twoFactorAuthentication.qrcode }}"/>
|
||||
<p>{{ twoFactorAuthentication.secret }}</p>
|
||||
@@ -184,7 +199,7 @@
|
||||
<br/>
|
||||
<form name="twoFactorAuthenticationEnableForm" role="form" novalidate ng-submit="twoFactorAuthentication.enable()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!twoFactorAuthenticationEnableForm.totpToken.$dirty && twoFactorAuthentication.error) || (twoFactorAuthenticationEnableForm.totpToken.$dirty && twoFactorAuthenticationEnableForm.totpToken.$invalid) }">
|
||||
<label class="control-label">Token</label>
|
||||
<label class="control-label">{{ 'profile.enable2FA.token' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!twoFactorAuthenticationEnableForm.totpToken.$dirty && twoFactorAuthentication.error) || (twoFactorAuthenticationEnableForm.totpToken.$dirty && twoFactorAuthenticationEnableForm.totpToken.$invalid)">
|
||||
<small>{{ twoFactorAuthentication.error }}</small>
|
||||
</div>
|
||||
@@ -194,8 +209,9 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="twoFactorAuthentication.enable()" ng-disabled="twoFactorAuthenticationEnableForm.$invalid || twoFactorAuthentication.busy"><i class="fa fa-circle-notch fa-spin" ng-show="twoFactorAuthentication.busy"></i> Enable</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal" ng-if="!twoFactorAuthentication.mandatory2FA">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="twoFactorAuthentication.enable()" ng-show="twoFactorAuthentication.secret" ng-disabled="twoFactorAuthenticationEnableForm.$invalid || twoFactorAuthentication.busy"><i class="fa fa-circle-notch fa-spin" ng-show="twoFactorAuthentication.busy"></i> {{ 'profile.enable2FA.enable' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="twoFactorAuthentication.getSecret()" ng-show="twoFactorAuthentication.mandatory2FAHelp" >{{ 'profile.enable2FA.setup2FA' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -206,23 +222,23 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Disable Two-Factor Authentication</h4>
|
||||
<h4 class="modal-title">{{ 'profile.disable2FA.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="twoFactorAuthenticationDisableForm" role="form" novalidate ng-submit="twoFactorAuthentication.disable()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!twoFactorAuthenticationDisableForm.password.$dirty && twoFactorAuthentication.error) || (twoFactorAuthenticationDisableForm.password.$dirty && twoFactorAuthenticationDisableForm.password.$invalid) }">
|
||||
<label class="control-label">Password</label>
|
||||
<label class="control-label">{{ 'profile.disable2FA.password' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!twoFactorAuthenticationDisableForm.password.$dirty && twoFactorAuthentication.error) || (twoFactorAuthenticationDisableForm.password.$dirty && twoFactorAuthenticationDisableForm.password.$invalid)">
|
||||
<small>{{ twoFactorAuthentication.error }}</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="twoFactorAuthentication.password" id="twoFactorAuthenticationPasswordInput" name="password" required autofocus>
|
||||
<input type="password" class="form-control" ng-model="twoFactorAuthentication.password" id="twoFactorAuthenticationPasswordInput" name="password" required autofocus password-reveal>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="twoFactorAuthenticationDisableForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="twoFactorAuthentication.disable()" ng-disabled="twoFactorAuthenticationDisableForm.$invalid || twoFactorAuthentication.busy"><i class="fa fa-circle-notch fa-spin" ng-show="twoFactorAuthentication.busy"></i> Disable</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="twoFactorAuthentication.disable()" ng-disabled="twoFactorAuthenticationDisableForm.$invalid || twoFactorAuthentication.busy"><i class="fa fa-circle-notch fa-spin" ng-show="twoFactorAuthentication.busy"></i> {{ 'profile.disable2FA.disable' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -233,21 +249,21 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Create App Password</h4>
|
||||
<h4 class="modal-title">{{ 'profile.createAppPassword.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-hide="appPasswordAdd.password">
|
||||
<form name="appPasswordAddForm" role="form" novalidate ng-submit="appPasswordAdd.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (appPasswordAddForm.name.$dirty && appPasswordAddForm.name.$invalid) || (!appPasswordAddForm.name.$dirty && appPasswordAdd.error.name)}">
|
||||
<label class="control-label">Password Name</label>
|
||||
<label class="control-label">{{ 'profile.createAppPassword.name' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!appPasswordAddForm.name.$dirty && appPasswordAdd.error.name) || (appPasswordAddForm.name.$dirty && appPasswordAddForm.name.$invalid)">
|
||||
<small ng-show="appPasswordAddForm.name.$error.required">A name is required</small>
|
||||
<small ng-show="appPasswordAddForm.name.$error.required">{{ 'profile.createAppPassword.errorNameRequired' | tr }}</small>
|
||||
<small ng-show="appPasswordAdd.error.name">{{ appPasswordAdd.error.name }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="appPasswordAdd.name" id="inputAppPasswordAddName" name="name" required autofocus>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (appPasswordAddForm.identifier.$dirty && appPasswordAddForm.identifier.$invalid) || (!appPasswordAddForm.identifier.$dirty && appPasswordAdd.error.identifier)}">
|
||||
<label class="control-label">App</label>
|
||||
<label class="control-label">{{ 'profile.createAppPassword.app' | tr }}</label>
|
||||
<select class="form-control" ng-model="appPasswordAdd.identifier" ng-options="a.id as a.label for a in appPassword.identifiers" required></select>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="appPasswordAddForm.$invalid"/>
|
||||
@@ -255,19 +271,18 @@
|
||||
</div>
|
||||
|
||||
<div ng-show="appPasswordAdd.password">
|
||||
Use the following password to authenticate against the app:
|
||||
{{ 'profile.createAppPassword.description' | tr }}
|
||||
<br/>
|
||||
<b ng-click-select>{{ appPasswordAdd.password.password }}</b>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<p>Please copy the password now. It won't be shown again for security purposes.</p>
|
||||
<p>{{ 'profile.createAppPassword.copyNow' | tr }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="appPasswordAdd.submit()" ng-hide="appPasswordAdd.password" ng-disabled="appPasswordAddForm.$invalid || appPasswordAdd.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="appPasswordAdd.busy"></i> Generate Password
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="appPasswordAdd.busy"></i> {{ 'profile.createAppPassword.generatePassword' | tr }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -279,15 +294,15 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Create API Token</h4>
|
||||
<h4 class="modal-title">{{ 'profile.createApiToken.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-hide="tokens.add.accessToken">
|
||||
<form name="apiTokenAddForm" role="form" novalidate ng-submit="tokens.add.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (apiTokenAddForm.name.$dirty && apiTokenAddForm.name.$invalid) || (!apiTokenAddForm.name.$dirty && tokens.add.error)}">
|
||||
<label class="control-label">API Token Name</label>
|
||||
<label class="control-label">{{ 'profile.createApiToken.name' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!apiTokenAddForm.name.$dirty && tokens.add.error) || (apiTokenAddForm.name.$dirty && apiTokenAddForm.name.$invalid)">
|
||||
<small ng-show="apiTokenAddForm.name.$error.required">A name is required</small>
|
||||
<small ng-show="apiTokenAddForm.name.$error.required">{{ 'profile.createApiToken.errorNameRequired' | tr }}</small>
|
||||
<small ng-show="tokens.add.error.name">{{ tokens.add.error }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" id="inputApiTokenName" ng-model="tokens.add.name" name="name" required autofocus>
|
||||
@@ -297,19 +312,19 @@
|
||||
</form>
|
||||
</div>
|
||||
<div ng-show="tokens.add.accessToken">
|
||||
New API token:
|
||||
{{ 'profile.createApiToken.description' | tr }}
|
||||
<br/>
|
||||
<b ng-click-select>{{ tokens.add.accessToken }}</b>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<p>Please copy the API token now. It won't be shown again for security purposes.</p>
|
||||
<p>{{ 'profile.createApiToken.copyNow' | tr }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="tokens.add.submit()" ng-hide="tokens.add.accessToken" ng-disabled="apiTokenAddForm.$invalid || tokens.add.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="tokens.add.busy"></i> Generate API Token
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="tokens.add.busy"></i> {{ 'profile.createApiToken.generateToken' | tr }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -319,76 +334,91 @@
|
||||
<div class="content">
|
||||
|
||||
<div class="text-left">
|
||||
<h1>Profile</h1>
|
||||
<h1>{{ 'profile.title' | tr }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-4" style="min-width: 150px;">
|
||||
<div class="settings-avatar" ng-click="avatarChange.showChangeAvatar()" style="background-image: url('{{ user.avatarUrl }}');">
|
||||
<div class="overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-8">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Username</td>
|
||||
<td class="text-right" style="vertical-align: top;">
|
||||
{{ user.username }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Display name</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">
|
||||
{{ user.displayName }} <a href="" ng-click="displayNameChange.show()" ng-hide="user.source"><i class="fa fa-edit text-small"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Primary email</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">
|
||||
{{ user.email }} <a href="" ng-click="emailchange.show()" ng-hide="user.source"><i class="fa fa-edit text-small"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Password recovery email</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">
|
||||
{{ user.fallbackEmail }} <a href="" ng-click="fallbackEmailChange.show()" ng-hide="user.source"><i class="fa fa-edit text-small"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-right" colspan="2" style="vertical-align: top;">
|
||||
<br/>
|
||||
<button class="btn btn-primary" ng-click="twoFactorAuthentication.show()">{{ user.twoFactorAuthenticationEnabled ? 'Disable 2FA' : 'Enable 2FA' }}</button>
|
||||
<button class="btn btn-primary" ng-click="passwordchange.show()" ng-hide="user.source">Change Password</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-xs-9">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">{{ 'main.username' | tr }}</td>
|
||||
<td class="text-right" style="vertical-align: top;">
|
||||
{{ user.username }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">{{ 'main.displayName' | tr }}</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">
|
||||
{{ user.displayName }} <a href="" ng-click="displayNameChange.show()" ng-hide="user.source || config.profileLocked"><i class="fa fa-edit text-small"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">{{ 'profile.passwordRecoveryEmail' | tr }}</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">
|
||||
{{ 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>
|
||||
<td colspan="2" class="text-right">
|
||||
<a href="" ng-click="sendPasswordReset()">{{ 'profile.passwordResetAction' | tr }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td colspan="2"> </td></tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: middle;">{{ 'profile.language' | tr }}</td>
|
||||
<td class="text-right" style="vertical-align: middle;">
|
||||
<multiselect ng-model="language" options="lang.display for lang in languages" data-multiple="false" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-right" colspan="2" style="vertical-align: top;">
|
||||
<br/>
|
||||
<button class="btn" ng-class="user.twoFactorAuthenticationEnabled ? 'btn-danger' : 'btn-success'" ng-click="twoFactorAuthentication.show()">{{ user.twoFactorAuthenticationEnabled ? 'profile.disable2FAAction' : 'profile.enable2FAAction' | tr }}</button>
|
||||
<button class="btn btn-primary" ng-click="passwordchange.show()" ng-hide="user.source">{{ 'profile.changePasswordAction' | tr }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>App Passwords<button class="btn btn-primary btn-sm pull-right" ng-click="appPasswordAdd.show()"><i class="fa fa-plus"></i> New Password</button></h3>
|
||||
<h3>{{ 'profile.appPasswords.title' | tr }}<button class="btn btn-primary btn-sm pull-right" ng-click="appPasswordAdd.show()"><i class="fa fa-plus"></i> {{ 'profile.appPasswords.newPassword' | tr }}</button></h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p>These passwords can be used as a security measure in desktop, email & mobile clients.</p>
|
||||
<p>{{ 'profile.appPasswords.description' | tr }}</p>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 45%">Name</th>
|
||||
<th style="width: 45%">App</th>
|
||||
<th style="width: 10%" class="text-right">Actions</th>
|
||||
<th style="width: 45%">{{ 'profile.appPasswords.name' | tr }}</th>
|
||||
<th style="width: 45%">{{ 'profile.appPasswords.app' | tr }}</th>
|
||||
<th style="width: 10%" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-show="appPassword.passwords.length === 0">
|
||||
<td colspan="3" class="text-center">{{ 'profile.appPasswords.noPasswordsPlaceholder' | tr }}</td>
|
||||
</tr>
|
||||
<tr ng-repeat="password in appPassword.passwords">
|
||||
<td class="text-left elide-table-cell">
|
||||
<span uib-tooltip="{{ password.creationTime | prettyLongDate }}" class="arrow">{{ password.name }}</span>
|
||||
@@ -397,7 +427,7 @@
|
||||
<span class="arrow">{{ password.label }}</span>
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-danger pull-right" ng-click="appPassword.del(password.id)" title="Delete Password"><i class="far fa-trash-alt"></i></button>
|
||||
<button class="btn btn-xs btn-danger pull-right" ng-click="appPassword.del(password.id)" uib-tooltip="{{ 'profile.appPasswords.deletePasswordTooltip' | tr }}"><i class="far fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -410,34 +440,36 @@
|
||||
<br ng-show="user.isAtLeastAdmin"/>
|
||||
|
||||
<div class="text-left" ng-show="user.isAtLeastAdmin">
|
||||
<h3>API Tokens <button class="btn btn-primary btn-sm pull-right" ng-click="tokens.add.show()"><i class="fa fa-plus"></i> New API Token</button></h3>
|
||||
<h3>{{ 'profile.apiTokens.title' | tr }} <button class="btn btn-primary btn-sm pull-right" ng-click="tokens.add.show()"><i class="fa fa-plus"></i> {{ 'profile.apiTokens.newApiToken' | tr }}</button></h3>
|
||||
</div>
|
||||
|
||||
<div class="card" ng-show="user.isAtLeastAdmin">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p ng-bind-html="'profile.apiTokens.description' | tr:{ apiDocsLink: 'https://docs.cloudron.io/api.html' }"></p>
|
||||
<table class="table table-hover" style="margin: 0;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 45%">Name</th>
|
||||
<th style="width: 45%">Expires At</th>
|
||||
<th style="width: 10%" class="text-right">Actions</th>
|
||||
<th style="width: 45%">{{ 'profile.apiTokens.name' | tr }}</th>
|
||||
<th style="width: 45%">{{ 'profile.apiTokens.lastUsed' | tr }}</th>
|
||||
<th style="width: 10%" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-show="tokens.apiTokens.length === 0">
|
||||
<td colspan="3" class="text-center">No API Tokens created</td>
|
||||
<td colspan="3" class="text-center">{{ 'profile.apiTokens.noTokensPlaceholder' | tr }}</td>
|
||||
</tr>
|
||||
<tr ng-repeat="token in tokens.apiTokens">
|
||||
<td class="elide-table-cell" style="text-overflow: ellipsis; white-space: nowrap;">
|
||||
{{ token.name || 'unnamed' }}
|
||||
</td>
|
||||
<td class="elide-table-cell" style="text-overflow: ellipsis; white-space: nowrap;">
|
||||
{{ token.expires | prettyShortDate }}
|
||||
<span ng-show="token.lastUsedTime">{{ token.lastUsedTime | prettyLongDate }}</span>
|
||||
<span ng-show="!token.lastUsedTime">{{ 'profile.apiTokens.neverUsed' | tr }}</span>
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-danger" ng-click="tokens.revokeToken(token)" uib-tooltip="Revoke Token"><i class="far fa-trash-alt"></i></button>
|
||||
<button class="btn btn-xs btn-danger" ng-click="tokens.revokeToken(token)" uib-tooltip="{{ 'profile.apiTokens.revokeTokenTooltip' | tr }}"><i class="far fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -450,15 +482,15 @@
|
||||
<br/>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>Login Tokens</h3>
|
||||
<h3>{{ 'profile.loginTokens.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p>You have {{ tokens.webadminTokens.length }} active web token(s) and {{ tokens.cliTokens.length }} CLI token(s).</p>
|
||||
<button class="btn btn-outline btn-danger pull-right" ng-click="tokens.revokeAllWebAndCliTokens()" ng-disabled="tokens.busy"><i class="fa fa-circle-notch fa-spin" ng-show="tokens.busy"></i> Logout From All</button>
|
||||
<p>{{ 'profile.loginTokens.description' | tr:{ webadminTokenCount: tokens.webadminTokens.length, cliTokenCount: cliTokens.length } }}</p>
|
||||
<button class="btn btn-outline btn-danger pull-right" ng-click="tokens.revokeAllWebAndCliTokens()" ng-disabled="tokens.busy"><i class="fa fa-circle-notch fa-spin" ng-show="tokens.busy"></i> {{ 'profile.loginTokens.logoutAll' | tr }}</button>
|
||||
<br/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+124
-41
@@ -1,14 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
/* global asyncForEach:false */
|
||||
/* global angular:false */
|
||||
/* global $:false */
|
||||
/* global async */
|
||||
/* global angular */
|
||||
/* global $ */
|
||||
|
||||
angular.module('Application').controller('ProfileController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
angular.module('Application').controller('ProfileController', ['$scope', '$translate', '$location', 'Client', '$timeout', function ($scope, $translate, $location, Client, $timeout) {
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.apps = Client.getInstalledApps();
|
||||
|
||||
$scope.language = '';
|
||||
$scope.languages = [];
|
||||
|
||||
$scope.$watch('language', function (newVal, oldVal) {
|
||||
if (newVal === oldVal) return;
|
||||
$translate.use(newVal.id);
|
||||
});
|
||||
|
||||
$scope.sendPasswordReset = function () {
|
||||
Client.sendSelfPasswordReset($scope.user.email, function (error) {
|
||||
if (error) return console.error('Failed to reset password:', error);
|
||||
|
||||
Client.notify($translate.instant('profile.passwordResetNotification.title'), $translate.instant('profile.passwordResetNotification.body', { email: $scope.user.fallbackEmail || $scope.user.email }), false, 'success');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.twoFactorAuthentication = {
|
||||
busy: false,
|
||||
error: null,
|
||||
@@ -16,6 +32,8 @@ angular.module('Application').controller('ProfileController', ['$scope', '$locat
|
||||
totpToken: '',
|
||||
secret: '',
|
||||
qrcode: '',
|
||||
mandatory2FA: false,
|
||||
mandatory2FAHelp: false, // show the initial help text when mandatory 2fa forces modal popup
|
||||
|
||||
reset: function () {
|
||||
$scope.twoFactorAuthentication.busy = false;
|
||||
@@ -24,6 +42,7 @@ angular.module('Application').controller('ProfileController', ['$scope', '$locat
|
||||
$scope.twoFactorAuthentication.totpToken = '';
|
||||
$scope.twoFactorAuthentication.secret = '';
|
||||
$scope.twoFactorAuthentication.qrcode = '';
|
||||
$scope.twoFactorAuthentication.mandatory2FAHelp = false;
|
||||
|
||||
$scope.twoFactorAuthenticationEnableForm.$setUntouched();
|
||||
$scope.twoFactorAuthenticationEnableForm.$setPristine();
|
||||
@@ -31,6 +50,25 @@ angular.module('Application').controller('ProfileController', ['$scope', '$locat
|
||||
$scope.twoFactorAuthenticationDisableForm.$setPristine();
|
||||
},
|
||||
|
||||
getSecret: function () {
|
||||
$scope.twoFactorAuthentication.mandatory2FAHelp = false;
|
||||
|
||||
Client.setTwoFactorAuthenticationSecret(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.twoFactorAuthentication.secret = result.secret;
|
||||
$scope.twoFactorAuthentication.qrcode = result.qrcode;
|
||||
});
|
||||
},
|
||||
|
||||
showMandatory2FA: function () {
|
||||
$scope.twoFactorAuthentication.reset();
|
||||
$scope.twoFactorAuthentication.mandatory2FA = true;
|
||||
$scope.twoFactorAuthentication.mandatory2FAHelp = true;
|
||||
|
||||
$('#twoFactorAuthenticationEnableModal').modal({ backdrop: 'static', keyboard: false }); // undimissable dialog
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.twoFactorAuthentication.reset();
|
||||
|
||||
@@ -39,12 +77,7 @@ angular.module('Application').controller('ProfileController', ['$scope', '$locat
|
||||
} else {
|
||||
$('#twoFactorAuthenticationEnableModal').modal('show');
|
||||
|
||||
Client.setTwoFactorAuthenticationSecret(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.twoFactorAuthentication.secret = result.secret;
|
||||
$scope.twoFactorAuthentication.qrcode = result.qrcode;
|
||||
});
|
||||
$scope.twoFactorAuthentication.getSecret();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -97,8 +130,8 @@ angular.module('Application').controller('ProfileController', ['$scope', '$locat
|
||||
busy: false,
|
||||
error: {},
|
||||
avatar: null,
|
||||
useGravatar: '',
|
||||
useGravatarOrig: '',
|
||||
type: '',
|
||||
typeOrig: '',
|
||||
pictureChanged: false,
|
||||
|
||||
getBlobFromImg: function (img, callback) {
|
||||
@@ -151,13 +184,13 @@ angular.module('Application').controller('ProfileController', ['$scope', '$locat
|
||||
});
|
||||
}
|
||||
|
||||
if ($scope.avatarChange.useGravatar) {
|
||||
Client.clearAvatar(done);
|
||||
} else {
|
||||
if ($scope.avatarChange.type === 'custom') {
|
||||
var img = document.getElementById('previewAvatar');
|
||||
$scope.avatarChange.getBlobFromImg(img, function (blob) {
|
||||
Client.changeAvatar(blob, done);
|
||||
});
|
||||
} else {
|
||||
Client.changeAvatar($scope.avatarChange.type, done);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -169,13 +202,19 @@ angular.module('Application').controller('ProfileController', ['$scope', '$locat
|
||||
|
||||
avatarChangeReset: function () {
|
||||
$scope.avatarChange.error.avatar = null;
|
||||
$scope.avatarChange.useGravatar = $scope.user.avatarUrl.indexOf('https://www.gravatar.com') === 0 ? 'true_string' : '';
|
||||
$scope.avatarChange.useGravatarOrig = $scope.avatarChange.useGravatar;
|
||||
|
||||
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 = '';
|
||||
}
|
||||
|
||||
$scope.avatarChange.typeOrig = $scope.avatarChange.type;
|
||||
document.getElementById('previewAvatar').src = $scope.avatarChange.type === 'custom' ? $scope.user.avatarUrl : '';
|
||||
$scope.avatarChange.pictureChanged = false;
|
||||
document.getElementById('previewAvatar').src = $scope.avatarChange.useGravatar ? '' : $scope.user.avatarUrl;
|
||||
$scope.avatarChange.avatar = $scope.avatarChange.useGravatar ? {} : {
|
||||
url: $scope.user.avatarUrl
|
||||
};
|
||||
$scope.avatarChange.avatar = null;
|
||||
$scope.avatarChange.busy = false;
|
||||
},
|
||||
|
||||
@@ -288,7 +327,6 @@ angular.module('Application').controller('ProfileController', ['$scope', '$locat
|
||||
return;
|
||||
}
|
||||
|
||||
// update user info in the background
|
||||
Client.refreshUserInfo();
|
||||
|
||||
$scope.emailchange.reset();
|
||||
@@ -299,13 +337,19 @@ angular.module('Application').controller('ProfileController', ['$scope', '$locat
|
||||
|
||||
$scope.fallbackEmailChange = {
|
||||
busy: false,
|
||||
error: {},
|
||||
error: {
|
||||
email: false,
|
||||
password: false
|
||||
},
|
||||
email: '',
|
||||
password: '',
|
||||
|
||||
reset: function () {
|
||||
$scope.fallbackEmailChange.busy = false;
|
||||
$scope.fallbackEmailChange.error.email = null;
|
||||
$scope.fallbackEmailChange.error.password = null;
|
||||
$scope.fallbackEmailChange.email = '';
|
||||
$scope.fallbackEmailChange.password = '';
|
||||
|
||||
$scope.fallbackEmailChangeForm.$setUntouched();
|
||||
$scope.fallbackEmailChangeForm.$setPristine();
|
||||
@@ -318,16 +362,28 @@ angular.module('Application').controller('ProfileController', ['$scope', '$locat
|
||||
|
||||
submit: function () {
|
||||
$scope.fallbackEmailChange.error.email = null;
|
||||
$scope.fallbackEmailChange.error.password = null;
|
||||
$scope.fallbackEmailChange.busy = true;
|
||||
|
||||
var data = {
|
||||
fallbackEmail: $scope.fallbackEmailChange.email
|
||||
fallbackEmail: $scope.fallbackEmailChange.email,
|
||||
password: $scope.fallbackEmailChange.password
|
||||
};
|
||||
|
||||
Client.updateProfile(data, function (error) {
|
||||
$scope.fallbackEmailChange.busy = false;
|
||||
|
||||
if (error) return console.error('Unable to change fallback email.', error);
|
||||
if (error) {
|
||||
if (error.statusCode === 412) {
|
||||
$scope.fallbackEmailChange.error.password = true;
|
||||
$scope.fallbackEmailChange.password = '';
|
||||
$scope.fallbackEmailChangeForm.password.$setPristine();
|
||||
$('#inputFallbackEmailChangePassword').focus();
|
||||
} else {
|
||||
console.error('Unable to change fallback email.', error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// update user info in the background
|
||||
Client.refreshUserInfo();
|
||||
@@ -363,11 +419,12 @@ angular.module('Application').controller('ProfileController', ['$scope', '$locat
|
||||
|
||||
submit: function () {
|
||||
$scope.appPasswordAdd.busy = true;
|
||||
$scope.appPasswordAdd.password = {};
|
||||
|
||||
Client.addAppPassword($scope.appPasswordAdd.identifier, $scope.appPasswordAdd.name, function (error, result) {
|
||||
$scope.appPasswordAdd.busy = false;
|
||||
|
||||
if (error) {
|
||||
if (error.statusCode === 400) {
|
||||
if (error.statusCode === 400 || error.statusCode === 409) {
|
||||
$scope.appPasswordAdd.error.name = error.message;
|
||||
$scope.appPasswordAddForm.name.$setPristine();
|
||||
$('#inputAppPasswordName').focus();
|
||||
@@ -377,7 +434,6 @@ angular.module('Application').controller('ProfileController', ['$scope', '$locat
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.appPasswordAdd.busy = false;
|
||||
$scope.appPasswordAdd.password = result;
|
||||
|
||||
$scope.appPassword.refresh();
|
||||
@@ -399,15 +455,20 @@ angular.module('Application').controller('ProfileController', ['$scope', '$locat
|
||||
$scope.appPassword.identifiers = [];
|
||||
var appsById = {};
|
||||
$scope.apps.forEach(function (app) {
|
||||
// ignore apps without ldap or with email
|
||||
if (!app.manifest.addons || !app.manifest.addons.ldap || app.manifest.addons.email || !app.sso) return;
|
||||
if (!app.manifest.addons) return;
|
||||
if (app.manifest.addons.email) return;
|
||||
|
||||
var ftp = app.manifest.addons.localstorage && app.manifest.addons.localstorage.ftp;
|
||||
var sso = app.sso && (app.manifest.addons.ldap || app.manifest.addons.proxyAuth);
|
||||
|
||||
if (!ftp && !sso) return;
|
||||
|
||||
appsById[app.id] = app;
|
||||
if (app.label) {
|
||||
$scope.appPassword.identifiers.push({ id: app.id, label: app.label + ' (' + app.fqdn + ')' });
|
||||
} else {
|
||||
$scope.appPassword.identifiers.push({ id: app.id, label: app.fqdn });
|
||||
}
|
||||
var labelSuffix = '';
|
||||
if (ftp && sso) labelSuffix = ' - SFTP & App Login';
|
||||
else if (ftp) labelSuffix = ' - SFTP Only';
|
||||
var label = app.label ? app.label + ' (' + app.fqdn + ')' + labelSuffix : app.fqdn + labelSuffix;
|
||||
$scope.appPassword.identifiers.push({ id: app.id, label: label });
|
||||
});
|
||||
$scope.appPassword.identifiers.push({ id: 'mail', label: 'Mail client' });
|
||||
|
||||
@@ -417,11 +478,9 @@ angular.module('Application').controller('ProfileController', ['$scope', '$locat
|
||||
var app = appsById[password.identifier];
|
||||
if (!app) return password.label = password.identifier + ' (App not found)';
|
||||
|
||||
if (app.label) {
|
||||
password.label = app.label + ' (' + app.fqdn + ')';
|
||||
} else {
|
||||
password.label = app.fqdn;
|
||||
}
|
||||
var ftp = app.manifest.addons && app.manifest.addons.localstorage && app.manifest.addons.localstorage.ftp;
|
||||
var labelSuffix = ftp ? ' - SFTP' : '';
|
||||
password.label = app.label ? app.label + ' (' + app.fqdn + ')' + labelSuffix : app.fqdn + labelSuffix;
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -512,7 +571,7 @@ angular.module('Application').controller('ProfileController', ['$scope', '$locat
|
||||
revokeAllWebAndCliTokens: function () {
|
||||
$scope.tokens.busy = true;
|
||||
|
||||
asyncForEach($scope.tokens.webadminTokens.concat($scope.tokens.cliTokens), function (token, callback) {
|
||||
async.eachSeries($scope.tokens.webadminTokens.concat($scope.tokens.cliTokens), function (token, callback) {
|
||||
// do not revoke token for this session, will do at the end with logout
|
||||
if (token.accessToken === Client.getToken()) return callback();
|
||||
|
||||
@@ -575,6 +634,19 @@ angular.module('Application').controller('ProfileController', ['$scope', '$locat
|
||||
Client.onReady(function () {
|
||||
$scope.appPassword.refresh();
|
||||
$scope.tokens.refresh();
|
||||
Client.refreshUserInfo(); // 2fa status might have changed by admin
|
||||
|
||||
$translate.onReady(function () {
|
||||
var usedLang = $translate.use() || $translate.fallbackLanguage();
|
||||
|
||||
$scope.languages = Client.getAvailableLanguages().map(function (l) {
|
||||
return {
|
||||
display: $translate.instant('lang.'+l, {}, undefined, 'en'),
|
||||
id: l
|
||||
};
|
||||
}).sort(function (a, b) { return a.display.localeCompare(b.display); });
|
||||
$scope.language = $scope.languages.find(function (l) { return l.id === usedLang; });
|
||||
});
|
||||
});
|
||||
|
||||
$('#avatarFileInput').get(0).onchange = function (event) {
|
||||
@@ -602,4 +674,15 @@ angular.module('Application').controller('ProfileController', ['$scope', '$locat
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
|
||||
if ($location.search().setup2fa) {
|
||||
// the form elements of the FormController won't appear in scope yet
|
||||
$timeout(function () { $scope.twoFactorAuthentication.showMandatory2FA(); }, 1000);
|
||||
} else {
|
||||
// don't let the user bypass 2FA by removing the 'setup2FA' in the url
|
||||
if (Client.getConfig().mandatory2FA && !Client.getUserInfo().twoFactorAuthenticationEnabled) {
|
||||
$location.path('/profile').search({ setup2fa: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}]);
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
<!-- Modal service configure -->
|
||||
<div class="modal fade" id="serviceConfigureModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'services.configure.title' | tr:{ name: serviceConfigure.service.displayName } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="serviceConfigureForm" role="form" novalidate ng-submit="serviceConfigure.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<p class="has-error text-center" ng-show="serviceConfigure.error">{{ serviceConfigure.error }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" style="display: block;" for="memoryLimit">
|
||||
{{ 'services.memoryLimit' | tr }}: <b>{{ serviceConfigure.memoryLimit / 1024 / 1024 + 'MB' }}</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<br>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="serviceConfigure.recoveryMode"><b>{{ 'services.configure.enableRecoveryMode' | tr }}</b></input>
|
||||
</label>
|
||||
</div>
|
||||
<p ng-bind-html="'services.configure.recoveryModeDescription' | tr:{ docsLink: 'https://docs.cloudron.io/troubleshooting/#unresponsive-service' }"></p>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="serviceConfigureForm.$invalid || serviceConfigure.busy"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="serviceConfigure.submit()" ng-disabled="serviceConfigureForm.$invalid || serviceConfigure.busy"><i class="fa fa-circle-notch fa-spin" ng-show="serviceConfigure.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<div class="text-left">
|
||||
<h1>{{ 'services.title' | tr }}
|
||||
<button class="btn btn-default pull-right" ng-click="refreshAll()">{{ 'services.refresh' | tr }}</button>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>{{ 'services.description' | tr }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="row ng-hide" ng-show="!servicesReady">
|
||||
<div class="col-md-12 text-center">
|
||||
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="servicesReady">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 5%;"></th>
|
||||
<th style="width: 20%">{{ 'services.service' | tr }}</th>
|
||||
<th style="width: 50%">{{ 'services.memoryUsage' | tr }}</th>
|
||||
<th style="width: 20%" class="text-center no-wrap">{{ 'services.memoryLimit' | tr }}</th>
|
||||
<th style="width: 5%" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><i class="fa fa-circle status-active" uib-tooltip="active"></i></td>
|
||||
<td class="elide-table-cell">cloudron</td>
|
||||
<td class="elide-table-cell"></td>
|
||||
<td class="elide-table-cell text-center"></td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<a class="btn btn-xs btn-default" href="/logs.html?id=box" target="_blank" uib-tooltip="{{ 'logs.title' | tr }}"><i class="fa fa-file-alt"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-repeat="service in services | filter:{ isRedis: false } | orderBy:'name'">
|
||||
<td>
|
||||
<span ng-switch on="service.status" ng-show="service.status">
|
||||
<span ng-switch-when="active">
|
||||
<i class="fa fa-circle status-active" uib-tooltip="active"></i>
|
||||
</span>
|
||||
<span ng-switch-when="starting">
|
||||
<i class="fa fa-circle status-starting" uib-tooltip="starting" ng-show="!service.config.recoveryMode"></i>
|
||||
<i class="fa fa-circle status-inactive" uib-tooltip="recovery mode" ng-show="service.config.recoveryMode"></i>
|
||||
</span>
|
||||
<span ng-switch-default>
|
||||
<i class="fa fa-circle status-error" uib-tooltip="{{ service.status }}"></i>
|
||||
</span>
|
||||
</span>
|
||||
<i class="fa fa-circle-notch fa-spin" ng-hide="service.status"></i>
|
||||
</td>
|
||||
<td class="elide-table-cell">
|
||||
{{ service.displayName }}
|
||||
</td>
|
||||
<td class="elide-table-cell">
|
||||
<div class="progress progress-striped" ng-show="service.config.memoryLimit">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ service.memoryPercent }}%"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="elide-table-cell text-center">
|
||||
<span ng-show="service.config.memoryLimit">{{ service.config.memoryLimit | prettyByteSize }}</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="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="{{ '/logs.html?id=' + service.name }}" target="_blank" uib-tooltip="{{ 'logs.title' | tr }}"><i class="fa fa-file-alt"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="hasRedisServices" ng-click="redisServicesExpanded = !redisServicesExpanded" class="hand">
|
||||
<td>
|
||||
<i class="fas fa-angle-right" ng-class="{'fa-rotate-90': redisServicesExpanded }"></i>
|
||||
</td>
|
||||
<td colspan="4">redis</td>
|
||||
</tr>
|
||||
<tr ng-show="redisServicesExpanded" ng-repeat="service in services | filter:{ isRedis: true } | orderBy:'name'">
|
||||
<td>
|
||||
<i class="fa fa-circle" uib-tooltip="{{ service.status }}" ng-class="{ 'status-active': service.status === 'active', 'status-starting': service.status === 'starting', 'status-error': (service.status !== 'starting' && service.status !== 'active') }" ng-show="service.status"></i>
|
||||
<i class="fa fa-circle-notch fa-spin" ng-hide="service.status"></i>
|
||||
</td>
|
||||
<td class="elide-table-cell">
|
||||
{{ service.displayName }}
|
||||
</td>
|
||||
<td class="elide-table-cell">
|
||||
<div class="progress progress-striped" ng-show="service.config.memoryLimit">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ service.memoryPercent }}%"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="elide-table-cell text-center">
|
||||
<span ng-show="service.config.memoryLimit">{{ service.config.memoryLimit | prettyByteSize }}</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-show="service.config.memoryLimit"><i class="fa fa-pencil-alt"></i></button>
|
||||
<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="{{ '/logs.html?id=' + service.name }}" target="_blank" uib-tooltip="{{ 'logs.title' | tr }}"><i class="fa fa-file-alt"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,183 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular */
|
||||
/* global $ */
|
||||
|
||||
angular.module('Application').controller('ServicesController', ['$scope', '$location', '$timeout', 'Client', function ($scope, $location, $timeout, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.servicesReady = false;
|
||||
$scope.services = [];
|
||||
$scope.hasRedisServices = false;
|
||||
$scope.redisServicesExpanded = false;
|
||||
$scope.memory = null;
|
||||
|
||||
function refresh(serviceName, callback) {
|
||||
callback = callback || function () {};
|
||||
|
||||
Client.getService(serviceName, function (error, result) {
|
||||
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;
|
||||
|
||||
service.status = result.status;
|
||||
service.config = result.config;
|
||||
service.memoryUsed = result.memoryUsed;
|
||||
service.memoryPercent = result.memoryPercent;
|
||||
|
||||
callback(null, service);
|
||||
});
|
||||
}
|
||||
|
||||
function waitForActive(serviceName) {
|
||||
refresh(serviceName, function (error, result) {
|
||||
if (result.status === 'active') return;
|
||||
|
||||
setTimeout(function () { waitForActive(serviceName); }, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.restartService = function (serviceName) {
|
||||
$scope.services.find(function (s) { return s.name === serviceName; }).status = 'starting';
|
||||
|
||||
Client.restartService(serviceName, function (error) {
|
||||
if (error && error.statusCode === 404) {
|
||||
Client.rebuildService(serviceName, function (error) {
|
||||
if (error) return Client.error(error);
|
||||
|
||||
// show "busy" indicator for 3 seconds to show some ui activity
|
||||
setTimeout(function () { waitForActive(serviceName); }, 3000);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
if (error) return Client.error(error);
|
||||
|
||||
// show "busy" indicator for 3 seconds to show some ui activity
|
||||
setTimeout(function () { waitForActive(serviceName); }, 3000);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.serviceConfigure = {
|
||||
error: null,
|
||||
busy: false,
|
||||
service: null,
|
||||
|
||||
// form model
|
||||
memoryLimit: 0,
|
||||
memoryTicks: [],
|
||||
|
||||
recoveryMode: false,
|
||||
|
||||
show: function (service) {
|
||||
$scope.serviceConfigure.reset();
|
||||
|
||||
$scope.serviceConfigure.service = service;
|
||||
$scope.serviceConfigure.memoryLimit = service.config.memoryLimit;
|
||||
$scope.serviceConfigure.recoveryMode = !!service.config.recoveryMode;
|
||||
|
||||
$scope.serviceConfigure.memoryTicks = [];
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
$('#serviceConfigureModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.serviceConfigure.busy = true;
|
||||
$scope.serviceConfigure.error = null;
|
||||
|
||||
var data = {
|
||||
memoryLimit: $scope.serviceConfigure.memoryLimit,
|
||||
recoveryMode: $scope.serviceConfigure.recoveryMode
|
||||
};
|
||||
|
||||
Client.configureService($scope.serviceConfigure.service.name, data, function (error) {
|
||||
$scope.serviceConfigure.busy = false;
|
||||
if (error) {
|
||||
$scope.serviceConfigure.error = error.message;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.serviceConfigure.recoveryMode === true) {
|
||||
refresh($scope.serviceConfigure.service.name);
|
||||
} else {
|
||||
waitForActive($scope.serviceConfigure.service.name);
|
||||
}
|
||||
|
||||
$('#serviceConfigureModal').modal('hide');
|
||||
$scope.serviceConfigure.reset();
|
||||
});
|
||||
},
|
||||
|
||||
resetToDefaults: function () {
|
||||
$scope.serviceConfigure.memoryLimit = 536870912; // 512MB default
|
||||
},
|
||||
|
||||
reset: function () {
|
||||
$scope.serviceConfigure.busy = false;
|
||||
$scope.serviceConfigure.error = null;
|
||||
$scope.serviceConfigure.service = null;
|
||||
|
||||
$scope.serviceConfigure.memoryLimit = 0;
|
||||
$scope.serviceConfigure.memoryTicks = [];
|
||||
|
||||
$scope.serviceConfigureForm.$setPristine();
|
||||
$scope.serviceConfigureForm.$setUntouched();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.refreshAll = function (callback) {
|
||||
Client.getServices(function (error, result) {
|
||||
if (error) return Client.error(error);
|
||||
|
||||
$scope.services = result.map(function (name) {
|
||||
var displayName = name;
|
||||
var isRedis = false;
|
||||
|
||||
if (name.indexOf('redis') === 0) {
|
||||
isRedis = true;
|
||||
var app = Client.getCachedAppSync(name.slice('redis:'.length));
|
||||
if (app) {
|
||||
displayName = 'Redis (' + (app.label || app.fqdn) + ')';
|
||||
} else {
|
||||
displayName = 'Redis (unknown app)';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: name,
|
||||
displayName: displayName,
|
||||
isRedis: isRedis
|
||||
};
|
||||
});
|
||||
$scope.hasRedisServices = !!$scope.services.find(function (service) { return service.isRedis; });
|
||||
|
||||
// just kick off the status fetching
|
||||
$scope.services.forEach(function (s) { refresh(s.name); });
|
||||
|
||||
if (callback) return callback();
|
||||
});
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
Client.memory(function (error, memory) {
|
||||
if (error) console.error(error);
|
||||
|
||||
$scope.memory = memory;
|
||||
|
||||
$scope.refreshAll(function () {
|
||||
$scope.servicesReady = true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}]);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user